Files
Lauren Hirata Singh 2276580e29 Name every llms.txt section index correctly so all are served (#5518)
## Why

Coverage fell from ~100% to **57%** after #5514. This is my regression,
and the cause is a Mintlify behavior I assumed rather than tested.

**Mintlify serves the exact filename `llms.txt` at any path, and 404s on
anything else.** The numbered variants my split produced were never
reachable:

| URL | status |
|---|---|
| `/oss/python/llms.txt` | 200 |
| `/oss/python/llms-2.txt` | **404** |
| `/oss/python/llms-3.txt` | **404** |
| `/langsmith/smith-api/llms.txt` | 200 |
| `/langsmith/smith-api/llms-2.txt` | **404** |

Only 4 of 9 section indexes were reachable. That left 1,229 visible
links — 1,112 across the four served files plus 117 inline in the root —
which is exactly what the audit reported: 883 sitemap pages plus 346
links not in the sitemap. The other **802 pages did not exist as far as
any agent was concerned**.

Confirmed the rule is filename-based, not path-based:
`.well-known/security.txt` is in the build and 404s, while
`/oss/python/llms.txt` byte-matches the file my build produced, so
Mintlify is serving my static file by name.

## What changed

Sections now split **by directory** rather than by filename. Every index
is written as `<prefix>/llms.txt`.

An oversized section sheds its heaviest child directories into their own
indexes until the remainder fits, instead of giving every child its own
file — a naive recursive split produced 109 files, most of them a single
page. Descriptions are also dropped from section indexes, which roughly
halves each entry; the root keeps them, since it has room.

| | before | after |
|---|---|---|
| root | 16,449 chars | 21,656 chars |
| section indexes | 9 (5 unreachable) | 56 (all reachable) |
| largest section | 40,101 | 42,263 |
| pages indexed | 2,038 (1,229 visible) | 2,038 |

All 56 are named `llms.txt`, none exceed 50,000 characters, and each is
one hop from the root so the coverage walker still reaches everything.

## The real gap

Every guard I added in #5514 checked **local files**, and nothing
checked they were **served**. The build-time validator passed happily
while a third of the index was unreachable in production.

`scripts/check_llms_urls.py` now verifies each section index returns 200
before it samples page URLs. Run against production today it correctly
reports 52 of 56 unserved, because the new paths are not deployed yet —
that is the check working. A unit test asserts no index is ever named
`llms-N.txt`.

## Still open: LLMS Full Size (a Mintlify routing issue, not a repo one)

The custom `llms-full.txt` **is** being picked up — Mintlify just serves
it on the wrong route. Probing all four endpoints:

| Route | Bytes | Whose |
|---|---:|---|
| `/llms.txt` | 16,449 | ours |
| `/.well-known/llms.txt` | 99,940 | Mintlify's, **truncated** |
| `/llms-full.txt` | 15,101,956 | Mintlify's |
| `/.well-known/llms-full.txt` | 6,246,717 | ours |

The override is applied to exactly one route per file, and they are
crossed. Verified by content rather than size:
`/.well-known/llms-full.txt` opens with `# Docs by LangChain`, while
`/.well-known/llms.txt` still ends with `_Note: this index was truncated
to stay under 100,000 characters; 569 pages and 3 OpenAPI specs
omitted._`. Neither `.well-known` file exists in our build (it contains
only `security.txt`), so both are served by Mintlify's own routing.

Two consequences:

1. **`LLMS Full Size` will keep failing** while the check reads
`/llms-full.txt`, because that route serves Mintlify's 15.1 MB file
regardless of the custom one. Nothing in this repo changes that.
2. **`/.well-known/llms.txt` serves a truncated index** that omits 569
pages, so any agent following the `.well-known` convention gets the
pre-fix file even though the root is correct.

This looks like a Mintlify bug and needs a support ticket, not a code
change here. Their documentation does not describe how custom files
interact with the `.well-known` mirrors. Separately, **no target size
for `llms-full.txt` is documented** by AFDocs or Mintlify, so even with
the routing fixed I cannot say whether 6.2 MB clears the bar.

## Other checks

- **Markdown Content Parity** (warning): unchanged diagnosis, not a
content defect. The only substantive gap is OpenAPI pages, where the
markdown is *richer* than the HTML. Set both parity thresholds to 0 for
informational mode.
- **LLMS TXT Directive Html** (1 of 15 pages), **Page Size Html** (1
page at 59K markdown from 1,195K HTML, 98% boilerplate), **Redirect
Behavior** (1 cross-host redirect): all single-page warnings in
Mintlify-controlled chrome or intentional `reference.langchain.com`
redirects. No repo change proposed.
- **Content Structure** and **Authentication**: skipped, not failures.
Both score 100.

## Validation

- 215 tests pass (1 new)
- `make lint` clean, `make broken-links` clean
- 56 section indexes, 0 named `llms-N.txt`, 0 over 50,000 chars
- 2,038 pages indexed, verified no duplicates and none lost

## AI disclosure

Authored with Claude Code (Claude Opus 5). The serving rule was
established by probing production rather than inferred, after the
previous PR shipped on an untested assumption about exactly this.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:49:55 -07:00

1049 lines
39 KiB
Python

"""Tests for the DocumentationBuilder class.
This module contains comprehensive tests for the DocumentationBuilder class,
covering all methods and edge cases including file extension handling,
directory structure preservation, and error conditions.
"""
import json
import re
from pathlib import Path
import pytest
from pipeline.core.builder import DocumentationBuilder
from tests.unit_tests.utils import File, file_system
def test_builder_initialization() -> None:
"""Test DocumentationBuilder initialization.
Verifies that the builder is correctly initialized with the provided
source and build directories, and that the copy_extensions set contains
the expected file extensions.
"""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
assert builder.src_dir == fs.src_dir
assert builder.build_dir == fs.build_dir
assert builder.copy_extensions == {
".mdx",
".md",
".json",
".svg",
".png",
".jpg",
".jpeg",
".gif",
".mp4",
".webm",
".yml",
".yaml",
".css",
".js",
".jsx",
".tsx",
".txt",
".woff2",
".woff",
".ttf",
".html",
}
def test_build_all_empty_directory() -> None:
"""Test building from an empty directory.
Verifies that the builder handles empty source directories correctly.
"""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
assert not fs.list_build_files()
def test_build_all_copies_tsx_snippets() -> None:
"""Test that local TSX snippet components are copied to build/snippets."""
files = [
File(
path="snippets/example-component.tsx",
content="export default function Example() { return null; }",
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
assert fs.build_file_exists("snippets/example-component.tsx")
def test_build_all_supported_files() -> None:
"""Test building all supported file types.
Verifies that the builder correctly copies all supported file types
while maintaining directory structure.
"""
files = [
# LangGraph (oss) files - both Python and JavaScript versions
File(path="oss/index.mdx", content="# Welcome"),
File(path="oss/config.json", content='{"name": "test"}'),
File(path="oss/guides/setup.md", content="# Setup Guide"),
# LangGraph Platform files
File(path="langgraph-platform/index.mdx", content="# Platform"),
File(path="langgraph-platform/guide.md", content="# Guide"),
# LangChain Labs files
File(path="labs/index.mdx", content="# Labs"),
# Shared files
File(path="images/logo.png", bytes=b"PNG_DATA"),
File(path="docs.json", content='{"name": "test"}'),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
# Verify all files were copied with correct structure
build_files = {str(p) for p in fs.list_build_files()}
# Python version of LangGraph files
assert "oss/python/index.mdx" in build_files
assert "oss/python/config.json" in build_files
assert "oss/python/guides/setup.md" in build_files
# JavaScript version of LangGraph files
assert "oss/javascript/index.mdx" in build_files
assert "oss/javascript/config.json" in build_files
assert "oss/javascript/guides/setup.md" in build_files
# LangGraph Platform files
assert "langgraph-platform/index.mdx" in build_files
assert "langgraph-platform/guide.md" in build_files
# LangChain Labs files
assert "labs/index.mdx" in build_files
# Shared files
assert "images/logo.png" in build_files
assert "docs.json" in build_files
# Total number of files should be:
# - 3 files * 2 versions (Python/JavaScript) for LangGraph
# - 2 files for Platform
# - 1 file for Labs
# - 2 shared files
assert len(build_files) == 11
def test_build_all_unsupported_files() -> None:
"""Test building with unsupported file types.
Verifies that the builder skips unsupported file types.
"""
files = [
# LangGraph files with supported and unsupported types
File(
path="oss/index.mdx",
content="# Welcome",
),
File(
path="oss/ignored.txt",
content="This should be ignored",
),
File(
path="oss/data.csv",
content="col1,col2\n1,2",
),
# Platform files with supported and unsupported types
File(
path="langgraph-platform/guide.md",
content="# Guide",
),
File(
path="langgraph-platform/ignored.txt",
content="This should be ignored",
),
# Labs files with supported and unsupported types
File(
path="labs/index.mdx",
content="# Labs",
),
File(
path="labs/data.csv",
content="col1,col2\n1,2",
),
# Shared files with supported and unsupported types
File(
path="images/logo.png",
bytes=b"PNG_DATA",
),
File(
path="ignored.txt",
content="This should be ignored",
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
# Verify only supported files were copied
build_files = {str(p) for p in fs.list_build_files()}
# Python version of LangGraph files (only .mdx)
assert "oss/python/index.mdx" in build_files
assert "oss/python/ignored.txt" not in build_files
assert "oss/python/data.csv" not in build_files
# JavaScript version of LangGraph files (only .mdx)
assert "oss/javascript/index.mdx" in build_files
assert "oss/javascript/ignored.txt" not in build_files
assert "oss/javascript/data.csv" not in build_files
# Platform files (only .md)
assert "langgraph-platform/guide.md" in build_files
assert "langgraph-platform/ignored.txt" not in build_files
# Labs files (only .mdx)
assert "labs/index.mdx" in build_files
assert "labs/data.csv" not in build_files
# Shared files (only .png)
assert "images/logo.png" in build_files
assert "ignored.txt" not in build_files
# Total number of files should be:
# - 1 file * 2 versions (Python/JavaScript) for LangGraph
# - 1 file for Platform
# - 1 file for Labs
# - 1 shared file
assert len(build_files) == 4
def test_build_single_file() -> None:
"""Test building a single file.
Verifies that the builder correctly copies a single file
when requested.
"""
files = [
File(
path="index.mdx",
content="# Welcome",
),
File(
path="config.json",
content='{"name": "test"}',
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_file(fs.src_dir / "index.mdx")
# Verify only the requested file was copied
build_files = fs.list_build_files()
assert len(build_files) == 1
assert Path("index.mdx") in build_files
assert not fs.build_file_exists("config.json")
def test_build_multiple_files() -> None:
"""Test building multiple specific files.
Verifies that the builder correctly copies multiple specified files
while maintaining directory structure.
"""
files = [
File(
path="index.mdx",
content="# Welcome",
),
File(
path="config.json",
content='{"name": "test"}',
),
File(
path="guides/setup.md",
content="# Setup Guide",
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_files(
[
fs.src_dir / "index.mdx",
fs.src_dir / "guides/setup.md",
],
)
# Verify only specified files were copied
build_files = fs.list_build_files()
assert len(build_files) == 2
assert Path("index.mdx") in build_files
assert Path("guides/setup.mdx") in build_files
assert not fs.build_file_exists("config.json")
def test_build_nonexistent_file() -> None:
"""Test building a nonexistent file.
Verifies that the builder handles attempts to build
nonexistent files gracefully.
"""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
with pytest.raises(AssertionError):
builder.build_file(fs.src_dir / "nonexistent.md")
def test_rewrite_oss_links_inserts_language() -> None:
"""Bare /oss/ links get the target language inserted after 'oss'."""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
content = "[Deep Agents](/oss/deepagents/overview)"
assert (
builder._rewrite_oss_links(content, "python")
== "[Deep Agents](/oss/python/deepagents/overview)"
)
assert (
builder._rewrite_oss_links(content, "js")
== "[Deep Agents](/oss/javascript/deepagents/overview)"
)
def test_rewrite_oss_links_preserves_existing_language() -> None:
"""Links that already specify a language are left untouched.
Regression test: unversioned langsmith pages are built with
target_language="python", so a link that already points at
/oss/python/... or /oss/javascript/... must not have a second
language segment inserted (which produced /oss/python/python/...).
"""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
py = "[LangChain](/oss/python/langchain/overview)"
js = "[LangChain](/oss/javascript/langchain/overview)"
# Building langsmith uses target_language="python"; both must survive.
assert builder._rewrite_oss_links(py, "python") == py
assert builder._rewrite_oss_links(js, "python") == js
def test_rewrite_oss_links_preserves_deepagents_code() -> None:
"""Deep Agents Code URLs stay language-agnostic (no python/javascript insert)."""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
link = "[Overview](/oss/deepagents/code/overview)"
assert builder._rewrite_oss_links(link, "python") == link
assert builder._rewrite_oss_links(link, "js") == link
# Sibling OSS paths still get the language prefix.
other = "[SDK](/oss/deepagents/quickstart)"
assert (
builder._rewrite_oss_links(other, "python")
== "[SDK](/oss/python/deepagents/quickstart)"
)
def test_unversioned_oss_code_builds_once() -> None:
"""Deep Agents Code pages build to oss/deepagents/code/, not per-language copies."""
files = [
File(
path="oss/deepagents/code/overview.mdx",
content=(
"---\ntitle: Code\n---\n\n"
"See [SDK](/oss/deepagents/quickstart) and "
"[Config](/oss/deepagents/code/configuration).\n"
),
),
File(
path="oss/deepagents/quickstart.mdx",
content="---\ntitle: Quickstart\n---\n\nSDK docs.\n",
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
code_src = fs.src_dir / "oss" / "deepagents" / "code" / "overview.mdx"
assert builder.is_unversioned_oss_file(code_src)
assert not builder.is_unversioned_oss_file(
fs.src_dir / "oss" / "deepagents" / "quickstart.mdx"
)
builder.build_file(code_src)
unversioned = fs.build_dir / "oss" / "deepagents" / "code" / "overview.mdx"
assert unversioned.exists()
assert not (
fs.build_dir / "oss" / "python" / "deepagents" / "code" / "overview.mdx"
).exists()
assert not (
fs.build_dir / "oss" / "javascript" / "deepagents" / "code" / "overview.mdx"
).exists()
content = unversioned.read_text()
assert "/oss/python/deepagents/quickstart" in content
assert "/oss/deepagents/code/configuration" in content
assert "/oss/python/deepagents/code/" not in content
def test_unversioned_oss_openwiki_builds_once() -> None:
"""OpenWiki pages build to oss/openwiki/, not per-language copies."""
files = [
File(
path="oss/openwiki/overview.mdx",
content=(
"---\ntitle: OpenWiki\n---\n\n"
"See [SDK](/oss/deepagents/quickstart) and "
"[Quickstart](/oss/openwiki/quickstart).\n"
),
),
File(
path="oss/deepagents/quickstart.mdx",
content="---\ntitle: Quickstart\n---\n\nSDK docs.\n",
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
openwiki_src = fs.src_dir / "oss" / "openwiki" / "overview.mdx"
assert builder.is_unversioned_oss_file(openwiki_src)
link = "[Overview](/oss/openwiki/overview)"
assert builder._rewrite_oss_links(link, "python") == link
assert builder._rewrite_oss_links(link, "js") == link
builder.build_file(openwiki_src)
unversioned = fs.build_dir / "oss" / "openwiki" / "overview.mdx"
assert unversioned.exists()
assert not (
fs.build_dir / "oss" / "python" / "openwiki" / "overview.mdx"
).exists()
assert not (
fs.build_dir / "oss" / "javascript" / "openwiki" / "overview.mdx"
).exists()
content = unversioned.read_text()
assert "/oss/python/deepagents/quickstart" in content
assert "/oss/openwiki/quickstart" in content
assert "/oss/python/openwiki/" not in content
def test_safe_source_files_skips_symlinks() -> None:
"""Source collection rejects symlinks so host files cannot be exfiltrated."""
files = [
File(
path="oss/openwiki/overview.mdx",
content="---\ntitle: OpenWiki\n---\n\nOverview.\n",
),
]
with file_system(files) as fs:
openwiki_dir = fs.src_dir / "oss" / "openwiki"
secret_target = fs.temp_dir / "outside-secret.txt"
secret_target.write_text("secret-value\n", encoding="utf-8")
symlink_path = openwiki_dir / "leaked.env"
symlink_path.symlink_to(secret_target)
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
collected = builder._safe_source_files(openwiki_dir)
assert openwiki_dir / "overview.mdx" in collected
assert symlink_path not in collected
assert all(not path.is_symlink() for path in collected)
builder._build_unversioned_oss_openwiki()
assert not (fs.build_dir / "oss" / "openwiki" / "leaked.env").exists()
overview = fs.build_dir / "oss" / "openwiki" / "overview.mdx"
assert overview.exists()
assert "secret-value" not in overview.read_text()
def test_rewrite_oss_links_skips_images_and_none() -> None:
"""Image paths and a None target language are passed through unchanged."""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
img = '<img src="/oss/images/diagram.png" />'
assert builder._rewrite_oss_links(img, "python") == img
link = "[x](/oss/deepagents/overview)"
assert builder._rewrite_oss_links(link, None) == link
def test_rewrite_snippet_imports_for_language() -> None:
"""MDX snippet imports are scoped under /snippets/{python|javascript}/."""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
content = (
"import RequiresLanggraphServer from "
"'/snippets/oss/requires-langgraph-server.mdx';\n"
'import { PatternEmbed } from "/snippets/pattern-embed.jsx"\n'
)
assert builder._rewrite_snippet_imports_for_language(content, "python") == (
"import RequiresLanggraphServer from "
"'/snippets/python/oss/requires-langgraph-server.mdx';\n"
'import { PatternEmbed } from "/snippets/pattern-embed.jsx"\n'
)
assert builder._rewrite_snippet_imports_for_language(content, "js") == (
"import RequiresLanggraphServer from "
"'/snippets/javascript/oss/requires-langgraph-server.mdx';\n"
'import { PatternEmbed } from "/snippets/pattern-embed.jsx"\n'
)
already = (
"import X from '/snippets/python/oss/requires-langgraph-server.mdx';\n"
)
assert builder._rewrite_snippet_imports_for_language(already, "js") == already
def test_snippet_oss_links_are_language_prefixed_not_relative() -> None:
"""Shared snippets with /oss/ links get absolute language-prefixed copies.
Regression for nested consumers such as langchain/frontend/branching-chat:
a fixed ``../langgraph/local-server`` relative link resolved incorrectly to
``/oss/{lang}/langchain/langgraph/local-server``.
"""
files = [
File(
path="snippets/oss/requires-langgraph-server.mdx",
content=(
"<Note>\n"
"This feature requires the "
"[LangGraph Agent Server](/oss/langgraph/local-server).\n"
"</Note>\n"
),
),
File(
path="oss/langchain/frontend/branching-chat.mdx",
content=(
"---\ntitle: Branching chat\n---\n\n"
"import RequiresLanggraphServer from "
"'/snippets/oss/requires-langgraph-server.mdx';\n\n"
"<RequiresLanggraphServer />\n"
),
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
default = (
fs.build_dir / "snippets" / "oss" / "requires-langgraph-server.mdx"
).read_text()
py_snippet = (
fs.build_dir
/ "snippets"
/ "python"
/ "oss"
/ "requires-langgraph-server.mdx"
).read_text()
js_snippet = (
fs.build_dir
/ "snippets"
/ "javascript"
/ "oss"
/ "requires-langgraph-server.mdx"
).read_text()
assert "/oss/python/langgraph/local-server" in default
assert "/oss/python/langgraph/local-server" in py_snippet
assert "/oss/javascript/langgraph/local-server" in js_snippet
assert "../langgraph/local-server" not in default
assert "../langgraph/local-server" not in py_snippet
assert "../langgraph/local-server" not in js_snippet
py_page = (
fs.build_dir
/ "oss"
/ "python"
/ "langchain"
/ "frontend"
/ "branching-chat.mdx"
).read_text()
js_page = (
fs.build_dir
/ "oss"
/ "javascript"
/ "langchain"
/ "frontend"
/ "branching-chat.mdx"
).read_text()
assert "from '/snippets/python/oss/requires-langgraph-server.mdx'" in py_page
assert (
"from '/snippets/javascript/oss/requires-langgraph-server.mdx'" in js_page
)
def test_rewrite_managed_deep_agents_links_inserts_language() -> None:
"""Managed Deep Agents links get the target language route."""
with file_system([]) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
content = (
"[Quickstart](/langsmith/managed-deep-agents-quickstart)\n"
'<Card href="/langsmith/managed-deep-agents-tools#example" />\n'
"[Python](/langsmith/python/managed-deep-agents-overview)"
)
python_content = builder._rewrite_managed_deep_agents_links(content, "python")
assert "/langsmith/python/managed-deep-agents-quickstart" in python_content
assert "/langsmith/python/managed-deep-agents-tools#example" in python_content
assert python_content.count("/langsmith/python/") == 3
js_content = builder._rewrite_managed_deep_agents_links(content, "js")
assert "/langsmith/javascript/managed-deep-agents-quickstart" in js_content
assert "/langsmith/javascript/managed-deep-agents-tools#example" in js_content
assert "/langsmith/python/managed-deep-agents-overview" in js_content
def test_build_all_creates_managed_deep_agents_language_routes() -> None:
"""Managed Deep Agents pages and snippets build for both languages."""
files = [
File(
path="langsmith/managed-deep-agents-overview.mdx",
content=(
"---\ntitle: Managed Deep Agents\n---\n\n"
"import NextSteps from "
"'/snippets/langsmith/managed-deep-agents-next-steps.mdx';\n\n"
"[Quickstart](/langsmith/managed-deep-agents-quickstart)\n\n"
"[Deep Agents](/oss/deepagents/overview)\n"
),
),
File(
path="langsmith/managed-deep-agents-quickstart.mdx",
content="---\ntitle: Quickstart\n---\n",
),
File(
path="snippets/langsmith/managed-deep-agents-next-steps.mdx",
content=(
"[Tools](/langsmith/managed-deep-agents-tools)\n"
"[Deep Agents](/oss/deepagents/overview)\n"
":::python\nPython only.\n:::\n"
":::js\nTypeScript only.\n:::\n"
),
),
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
# Unversioned routes are redirects only; do not emit orphaned pages.
assert not (
fs.build_dir / "langsmith" / "managed-deep-agents-overview.mdx"
).exists()
assert not (
fs.build_dir / "langsmith" / "managed-deep-agents-quickstart.mdx"
).exists()
python_page = (
fs.build_dir / "langsmith" / "python" / "managed-deep-agents-overview.mdx"
).read_text()
js_page = (
fs.build_dir
/ "langsmith"
/ "javascript"
/ "managed-deep-agents-overview.mdx"
).read_text()
assert "/langsmith/python/managed-deep-agents-quickstart" in python_page
assert "/langsmith/javascript/managed-deep-agents-quickstart" in js_page
assert "/oss/python/deepagents/overview" in python_page
assert "/oss/javascript/deepagents/overview" in js_page
assert (
"from '/snippets/python/langsmith/managed-deep-agents-next-steps.mdx'"
in python_page
)
assert (
"from '/snippets/javascript/langsmith/managed-deep-agents-next-steps.mdx'"
in js_page
)
python_snippet = (
fs.build_dir
/ "snippets"
/ "python"
/ "langsmith"
/ "managed-deep-agents-next-steps.mdx"
).read_text()
js_snippet = (
fs.build_dir
/ "snippets"
/ "javascript"
/ "langsmith"
/ "managed-deep-agents-next-steps.mdx"
).read_text()
assert "/langsmith/python/managed-deep-agents-tools" in python_snippet
assert "Python only." in python_snippet
assert "TypeScript only." not in python_snippet
assert "/langsmith/javascript/managed-deep-agents-tools" in js_snippet
assert "TypeScript only." in js_snippet
assert "Python only." not in js_snippet
def test_build_all_writes_llms_txt() -> None:
"""Test that build_all emits a custom llms.txt indexing every page.
Mintlify truncates its auto-generated llms.txt at 100,000 characters, so
the pipeline writes its own uncapped file at the build root.
"""
files: list[File] = [
{
"path": "langsmith/tracing.mdx",
"content": "---\ntitle: Tracing\ndescription: Trace runs.\n---\n\nBody.\n",
},
{
"path": "langsmith/hidden.mdx",
"content": "---\ntitle: Hidden\nnoindex: true\n---\n\nBody.\n",
},
{
"path": "snippets/shared.mdx",
"content": "---\ntitle: Shared\n---\n\nSnippet body.\n",
},
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
llms_txt = (fs.build_dir / "llms.txt").read_text(encoding="utf-8")
assert llms_txt.startswith("# ")
assert (
"- [Tracing](https://docs.langchain.com/langsmith/tracing.md): Trace runs."
in llms_txt
)
# noindex pages and snippets are not real pages, so they stay out.
assert "hidden.md" not in llms_txt
assert "snippets/shared.md" not in llms_txt
def test_tag_slug_preserves_underscores() -> None:
"""Test that OpenAPI tag slugs keep underscores but normalize case and spaces.
The LangSmith spec carries both `annotation-queues` and `annotation_queues`
as distinct tags that Mintlify renders to different directories, so
collapsing them together would generate URLs for pages that do not exist.
"""
slug = DocumentationBuilder._tag_slug
assert slug("annotation_queues") == "annotation_queues"
assert slug("annotation-queues") == "annotation-queues"
assert slug("SCIM Tokens") == "scim-tokens"
assert slug("A2A") == "a2a"
def test_slugify_drops_apostrophes() -> None:
"""Test that apostrophes are removed rather than turned into separators."""
slug = DocumentationBuilder._slugify
assert slug("Get the authenticated user's provider user ID") == (
"get-the-authenticated-users-provider-user-id"
)
assert slug("Get company info") == "get-company-info"
def test_openapi_entries_skip_hidden_and_number_duplicates() -> None:
"""Test that hidden operations are omitted and duplicate slugs get suffixes.
Mintlify renders no page for `x-hidden` operations, and disambiguates two
operations sharing a summary with a numeric suffix instead of dropping one.
"""
spec = {
"paths": {
"/a": {"get": {"tags": ["orgs"], "summary": "Get info", "responses": {}}},
"/b": {"get": {"tags": ["orgs"], "summary": "Get info", "responses": {}}},
"/c": {
"get": {
"tags": ["fleet orgs"],
"summary": "Hidden op",
"responses": {},
"x-hidden": True,
}
},
}
}
docs_json = {
"navigation": {
"pages": [
{
"group": "REST API",
"openapi": {
"source": "langsmith/spec.json",
"directory": "langsmith/api",
},
}
]
}
}
files: list[File] = [
{"path": "docs.json", "content": json.dumps(docs_json)},
{"path": "langsmith/spec.json", "content": json.dumps(spec)},
{"path": "langsmith/page.mdx", "content": "---\ntitle: Page\n---\n\nBody.\n"},
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
slugs = [slug for _, slug, _ in builder._openapi_entries()]
assert slugs == ["langsmith/api/orgs/get-info", "langsmith/api/orgs/get-info-1"]
def test_llms_txt_splits_large_sections_into_section_indexes() -> None:
"""Test that a large section becomes linked section files, not root bulk.
AFDocs passes `llms-txt-size` only under 50,000 characters, and its
coverage walker descends exactly one level into linked .txt files, so
section indexes must sit one hop from the root and must not nest further.
"""
files: list[File] = [
{
"path": f"langsmith/page-{i:03d}.mdx",
"content": (
f"---\ntitle: Page {i}\ndescription: {'x' * 250}\n---\n\nBody.\n"
),
}
for i in range(400)
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
root = (fs.build_dir / "llms.txt").read_text(encoding="utf-8")
sections = sorted(
p.name for p in (fs.build_dir / "langsmith").glob("llms*.txt")
)
# Root stays under the pass threshold and delegates to section files.
assert len(root) < 50_000
assert sections, "expected at least one section index"
for name in sections:
section = (fs.build_dir / "langsmith" / name).read_text(encoding="utf-8")
assert len(section) < 50_000
assert "llms.txt" not in section.replace("# ", ""), "must not nest deeper"
assert f"({builder._SITE_URL}/langsmith/{name})" in root
# Every page is listed exactly once across root plus sections.
listed = re.findall(r"\((https://\S+?\.md)\)", root)
for name in sections:
body = (fs.build_dir / "langsmith" / name).read_text(encoding="utf-8")
listed += re.findall(r"\((https://\S+?\.md)\)", body)
assert len(listed) == len(set(listed)) == 400
def test_llms_full_txt_splits_languages_and_inlines_snippets() -> None:
"""Test that llms-full.txt splits language corpora and expands snippets.
Mintlify expands snippet imports when it renders, so a corpus built from
the raw build tree would silently drop content from every page that
imports one. The root file must also open with the site title as an H1.
"""
files: list[File] = [
{"path": "docs.json", "content": json.dumps({"name": "Test Docs"})},
{
"path": "snippets/shared-block.mdx",
"content": "UNIQUE_SNIPPET_CONTENT\n",
},
{
"path": "oss/guide.mdx",
"content": (
"---\ntitle: Guide\n---\n\n"
"import SharedBlock from '/snippets/shared-block.mdx';\n\n"
"Intro.\n\n<SharedBlock />\n"
),
},
{"path": "langsmith/core.mdx", "content": "---\ntitle: Core\n---\n\nCore.\n"},
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
root = (fs.build_dir / "llms-full.txt").read_text(encoding="utf-8")
py = (fs.build_dir / "oss/python/llms-full.txt").read_text(encoding="utf-8")
js = (fs.build_dir / "oss/javascript/llms-full.txt").read_text(encoding="utf-8")
# Root opens with the site title and points at the language corpora.
assert root.startswith("# Test Docs")
assert "oss/python/llms-full.txt" in root
assert "oss/javascript/llms-full.txt" in root
# Language pages live in their own corpus, not the root.
assert "Source: https://docs.langchain.com/oss/python/guide" in py
assert "Source: https://docs.langchain.com/oss/python/guide" not in root
assert "Source: https://docs.langchain.com/langsmith/core" in root
# Snippets are inlined, and the import line itself is gone.
assert "UNIQUE_SNIPPET_CONTENT" in py
assert "UNIQUE_SNIPPET_CONTENT" in js
assert "import SharedBlock" not in py
def _write_index(build_dir: Path, name: str, body: str) -> None:
"""Write an llms index file into a build directory for validator tests."""
path = build_dir / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
def test_validate_llms_indexes_accepts_a_well_formed_index() -> None:
"""Test that the validator passes a root plus section index that is correct."""
with file_system([]) as fs:
fs.build_dir.mkdir(parents=True, exist_ok=True)
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
site = builder._SITE_URL
_write_index(
fs.build_dir,
"llms.txt",
f"# Site\n\n- [A]({site}/a.md)\n- [Section]({site}/oss/llms.txt)\n",
)
_write_index(fs.build_dir, "oss/llms.txt", f"# Site\n\n- [B]({site}/b.md)\n")
builder._validate_llms_indexes(2) # does not raise
def test_validate_llms_indexes_rejects_oversized_root() -> None:
"""Test that a root over the truncation threshold fails the build."""
with file_system([]) as fs:
fs.build_dir.mkdir(parents=True, exist_ok=True)
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
padding = "x" * builder._LLMS_MAX
_write_index(
fs.build_dir,
"llms.txt",
f"# Site\n\n{padding}\n- [A]({builder._SITE_URL}/a.md)\n",
)
with pytest.raises(ValueError, match="over the 50,000 threshold"):
builder._validate_llms_indexes(1)
def test_validate_llms_indexes_rejects_second_level_nesting() -> None:
"""Test that a section index linking to further .txt files fails.
Coverage walkers descend one level, so pages behind a second hop drop out
of the index entirely.
"""
with file_system([]) as fs:
fs.build_dir.mkdir(parents=True, exist_ok=True)
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
site = builder._SITE_URL
_write_index(
fs.build_dir, "llms.txt", f"# Site\n\n- [S]({site}/oss/llms.txt)\n"
)
_write_index(
fs.build_dir,
"oss/llms.txt",
f"# Site\n\n- [A]({site}/a.md)\n- [Deeper]({site}/oss/py/llms.txt)\n",
)
with pytest.raises(ValueError, match="descend only one level"):
builder._validate_llms_indexes(1)
def test_validate_llms_indexes_rejects_duplicate_and_missing_pages() -> None:
"""Test that a page listed twice, or a page count mismatch, fails."""
with file_system([]) as fs:
fs.build_dir.mkdir(parents=True, exist_ok=True)
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
site = builder._SITE_URL
_write_index(
fs.build_dir, "llms.txt", f"# Site\n\n- [S]({site}/oss/llms.txt)\n"
)
_write_index(
fs.build_dir,
"oss/llms.txt",
f"# Site\n\n- [A]({site}/a.md)\n- [A again]({site}/a.md)\n",
)
with pytest.raises(ValueError, match="listed more than once"):
builder._validate_llms_indexes(1)
_write_index(fs.build_dir, "oss/llms.txt", f"# Site\n\n- [A]({site}/a.md)\n")
with pytest.raises(ValueError, match="but 5 were built"):
builder._validate_llms_indexes(5)
def test_page_body_rejects_snippet_imports_outside_the_build_tree() -> None:
"""Test that a traversing snippet import cannot read files outside build/.
Snippet import paths come from MDX text, which is editable in a pull
request, and CI commits build/ to a pushed preview branch. Path joins do
not collapse "..", so an unvalidated import would publish any readable
file on the build host.
"""
files: list[File] = [
# A real snippet, so build/snippets/ exists and ".." can actually
# traverse out of it. Without this the read fails for the wrong reason
# and the test passes even when the containment check is removed.
{"path": "snippets/real.mdx", "content": "Legitimate snippet.\n"},
{
"path": "langsmith/evil.mdx",
"content": (
"---\ntitle: Evil\n---\n\n"
"import Leak from '/snippets/../../secret.txt';\n\n"
"Body.\n\n<Leak />\n"
),
},
]
with file_system(files) as fs:
secret = fs.temp_dir / "secret.txt"
secret.write_text("TOP_SECRET_TOKEN_VALUE", encoding="utf-8")
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
page = fs.build_dir / "langsmith/evil.mdx"
body = builder._page_body(page)
corpus = (fs.build_dir / "llms-full.txt").read_text(encoding="utf-8")
# The traversing import is dropped, not followed.
assert "TOP_SECRET_TOKEN_VALUE" not in body
assert "TOP_SECRET_TOKEN_VALUE" not in corpus
# The page itself still renders, minus the rejected import.
assert "Body." in body
def test_resolve_within_blocks_escapes_and_allows_children() -> None:
"""Test the containment helper directly, including symlink-free traversal."""
with file_system([]) as fs:
fs.build_dir.mkdir(parents=True, exist_ok=True)
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
(fs.build_dir / "snippets").mkdir(parents=True, exist_ok=True)
(fs.build_dir / "snippets/ok.mdx").write_text("fine", encoding="utf-8")
(fs.temp_dir / "outside.txt").write_text("nope", encoding="utf-8")
root = fs.build_dir / "snippets"
inside = builder._resolve_within(fs.build_dir / "snippets/ok.mdx", root)
escape = builder._resolve_within(
fs.build_dir / "snippets/../../outside.txt", root
)
assert inside is not None
assert escape is None
def test_section_indexes_are_always_named_llms_txt() -> None:
"""Test that oversized sections split by directory, never by filename.
Mintlify serves the exact filename llms.txt at any path but 404s on
anything else, so numbered variants like llms-2.txt are silently
unreachable and every page in them drops out of coverage.
"""
# Enough pages across real subdirectories to force a split.
files: list[File] = [
{
"path": f"langsmith/{area}/page-{i:03d}.mdx",
"content": f"---\ntitle: {area} {i}\n---\n\nBody.\n",
}
for area in ("alpha", "beta", "gamma")
for i in range(250)
]
with file_system(files) as fs:
builder = DocumentationBuilder(fs.src_dir, fs.build_dir)
builder.build_all()
indexes = sorted(
p.relative_to(fs.build_dir).as_posix()
for p in fs.build_dir.rglob("llms*.txt")
if p.name != "llms-full.txt"
)
root = (fs.build_dir / "llms.txt").read_text(encoding="utf-8")
assert len(indexes) > 1, "expected the section to split"
# Every index, at every depth, is named exactly llms.txt.
for path in indexes:
assert path.endswith("llms.txt"), path
assert "llms-" not in path, f"numbered index would 404: {path}"
# And each split index is reachable in one hop from the root.
for path in indexes:
if path != "llms.txt":
assert f"({builder._SITE_URL}/{path})" in root