Files
OpenShell/tasks/scripts/sync_docs_website_test.py
Johnny Greco 8934b74a85 fix(docs): sync redirects with versioned snapshots (#3754)
* fix(docs): sync redirects with versioned snapshots

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* fix(docs): make redirect validation types explicit

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

---------

Signed-off-by: Johnny Greco <jogreco@nvidia.com>
2026-09-28 00:27:56 +00:00

1172 lines
38 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Tests for tasks/scripts/sync_docs_website.py.
Run via `mise run test:docs-website`, which provides pytest + PyYAML through
`uv run --with ...`. pytest puts this file's directory on sys.path, so the
sibling script imports directly as `sync_docs_website`.
"""
from __future__ import annotations
from argparse import Namespace
from pathlib import Path
from typing import cast
import pytest
import sync_docs_website as sdw
import yaml
def read_yaml(path: Path) -> dict:
return yaml.safe_load(path.read_text(encoding="utf-8"))
def read_workflow(name: str) -> dict:
path = Path(__file__).resolve().parents[2] / ".github" / "workflows" / name
return yaml.load(path.read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
def test_release_workflows_sync_and_publish_docs_once() -> None:
dev = read_workflow("release-dev.yml")
tag = read_workflow("release-tag.yml")
dev_job = dev["jobs"]["publish-fern-docs"]
tag_job = tag["jobs"]["publish-fern-docs"]
assert dev_job["needs"] == [
"compute-versions",
"release-dev",
"release-helm",
"trigger-wheel-publish",
]
assert dev_job["uses"] == "./.github/workflows/sync-docs.yml"
assert dev_job["with"]["channel"] == "dev"
assert (
dev_job["with"]["release_version"]
== "${{ needs.compute-versions.outputs.docs_version }}"
)
assert dev_job["with"]["publish"] == "true"
assert dev_job["with"]["display_name"] == "Dev"
assert "availability" not in dev_job["with"]
assert tag_job["needs"] == [
"compute-versions",
"release",
"publish-sdk-typescript",
"release-helm",
"trigger-wheel-publish",
]
assert tag_job["uses"] == "./.github/workflows/sync-docs.yml"
assert tag_job["with"]["channel"] == "stable"
assert (
tag_job["with"]["release_version"]
== "${{ needs.compute-versions.outputs.semver }}"
)
assert (
tag_job["with"]["version_slug"]
== "v${{ needs.compute-versions.outputs.semver }}"
)
assert (
tag_job["with"]["display_name"]
== "Latest (v${{ needs.compute-versions.outputs.semver }})"
)
assert tag_job["with"]["publish"] == "true"
assert "is_prerelease != 'true'" in tag_job["if"]
for workflow_name in ("release-dev.yml", "release-tag.yml"):
workflow_path = (
Path(__file__).resolve().parents[2]
/ ".github"
/ "workflows"
/ workflow_name
)
workflow_text = workflow_path.read_text(encoding="utf-8")
assert workflow_text.count("uses: ./.github/workflows/sync-docs.yml") == 1
assert "fern generate --docs" not in workflow_text
def test_sync_workflow_serializes_sync_and_publish() -> None:
workflow = read_workflow("sync-docs.yml")
triggers = workflow["on"]
publish_input = triggers["workflow_call"]["inputs"]["publish"]
assert publish_input["type"] == "boolean"
assert publish_input["default"] == "false"
assert workflow["concurrency"]["group"] == "docs-website"
assert workflow["concurrency"]["queue"] == "max"
publish_workflow = read_workflow("publish-docs-website.yml")
assert publish_workflow["concurrency"]["queue"] == "max"
steps = workflow["jobs"]["sync"]["steps"]
step_names = [step["name"] for step in steps]
assert step_names.index("Commit docs website changes") < step_names.index(
"Publish Fern docs"
)
publish_step = next(step for step in steps if step["name"] == "Publish Fern docs")
assert publish_step["if"] == "${{ inputs.publish }}"
assert publish_step["working-directory"] == "docs-website/fern"
update_step = next(step for step in steps if step["name"] == "Update docs snapshot")
assert "git -C source rev-parse HEAD" in update_step["run"]
assert '--source-sha "$SOURCE_SHA"' in update_step["run"]
def test_resolve_slug_channels() -> None:
assert sdw.resolve_slug("dev", "") == "dev"
assert sdw.resolve_slug("latest", "") == "latest"
assert sdw.resolve_slug("stable", "v0.1.0") == "v0.1.0"
assert sdw.resolve_slug("version", "v0.0.36") == "v0.0.36"
def test_resolve_slug_version_requires_slug() -> None:
with pytest.raises(ValueError):
sdw.resolve_slug("version", "")
def test_resolve_slug_rejects_unsafe_characters() -> None:
# Guards the slug that becomes a directory name (pages-<slug>).
with pytest.raises(ValueError):
sdw.resolve_slug("version", "../escape")
with pytest.raises(ValueError):
sdw.resolve_slug("version", "v1 0")
def test_resolve_display_name() -> None:
assert sdw.resolve_display_name("dev", "dev", "main", "") == "dev"
assert (
sdw.resolve_display_name("latest", "latest", "v0.0.57", "")
== "Latest (v0.0.57)"
)
assert sdw.resolve_display_name("latest", "latest", "abc123", "") == "Latest"
assert sdw.resolve_display_name("version", "v0.0.36", "v0.0.36", "") == "v0.0.36"
assert sdw.resolve_display_name("dev", "dev", "main", "Custom") == "Custom"
def test_resolve_availability() -> None:
assert sdw.resolve_availability("") is None
assert sdw.resolve_availability("beta") == "beta"
assert sdw.resolve_availability("deprecated") == "deprecated"
with pytest.raises(ValueError):
sdw.resolve_availability("alpha")
def test_parse_and_render_versions_preserves_version_settings() -> None:
raw_versions = [
{
"display-name": "v0.0.36",
"path": "./versions/v0.0.36.yml",
"slug": "v0.0.36",
"availability": "deprecated",
"announcement": {"message": "Upgrade to the latest version."},
}
]
entries = sdw.parse_versions(raw_versions)
assert entries == [
sdw.VersionEntry(
"v0.0.36",
"v0.0.36",
"./versions/v0.0.36.yml",
"deprecated",
{"message": "Upgrade to the latest version."},
)
]
assert sdw.render_versions(entries) == raw_versions
def test_sync_global_announcement_applies_source_config(tmp_path: Path) -> None:
source_docs_yml = tmp_path / "source.yml"
target_docs_yml = tmp_path / "target.yml"
source_docs_yml.write_text(
yaml.safe_dump(
{
"announcement": {"message": "Current global announcement."},
"versions": [],
}
),
encoding="utf-8",
)
target_docs_yml.write_text(
yaml.safe_dump(
{
"announcement": {"message": "Stale global announcement."},
"versions": [
{
"display-name": "Latest (v0.0.116)",
"path": "./versions/latest.yml",
"slug": "latest",
},
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
"announcement": {"message": "Development docs."},
},
],
}
),
encoding="utf-8",
)
sdw.sync_global_announcement(source_docs_yml, target_docs_yml)
target_data = read_yaml(target_docs_yml)
assert target_data["announcement"] == {"message": "Current global announcement."}
assert target_data["versions"] == [
{
"display-name": "Latest (v0.0.116)",
"path": "./versions/latest.yml",
"slug": "latest",
},
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
"announcement": {"message": "Development docs."},
},
]
source_docs_yml.write_text(
yaml.safe_dump({"versions": []}),
encoding="utf-8",
)
sdw.sync_global_announcement(source_docs_yml, target_docs_yml)
target_data = read_yaml(target_docs_yml)
assert "announcement" not in target_data
versions = target_data["versions"]
assert "announcement" not in versions[0]
assert versions[1]["announcement"] == {"message": "Development docs."}
def test_source_version_announcement_maps_single_source_version_to_channel(
tmp_path: Path,
) -> None:
docs_yml = tmp_path / "docs.yml"
docs_yml.write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Dev",
"path": "../docs/index.yml",
"slug": "dev",
"announcement": {"message": "Snapshot announcement."},
}
]
}
),
encoding="utf-8",
)
expected = {"message": "Snapshot announcement."}
assert sdw.source_version_announcement(docs_yml, "latest") == expected
assert sdw.source_version_announcement(docs_yml, "dev") == expected
assert sdw.source_version_announcement(docs_yml, "v0.0.116") == expected
def test_ordered_entries_pins_latest_then_dev() -> None:
existing = [
sdw.VersionEntry("v0.0.116", "v0.0.116", "./versions/v0.0.116.yml"),
sdw.VersionEntry("dev", "dev", "./versions/dev.yml"),
sdw.VersionEntry("v1.4.0", "v1.4.0", "./versions/v1.4.0.yml"),
sdw.VersionEntry("v1.4.2", "v1.4.2", "./versions/v1.4.2.yml"),
sdw.VersionEntry("v1.4.1", "v1.4.1", "./versions/v1.4.1.yml"),
sdw.VersionEntry("legacy", "legacy", "./versions/legacy.yml"),
]
updated = sdw.VersionEntry("latest", "Latest", "./versions/latest.yml")
ordered = [entry.slug for entry in sdw.ordered_entries(existing, updated)]
assert ordered == [
"latest",
"dev",
"v1.4.2",
"v1.4.1",
"v1.4.0",
"v0.0.116",
"legacy",
]
refreshed = sdw.VersionEntry("v1.4.1", "v1.4.1", "./versions/v1.4.1.yml")
refreshed_order = [entry.slug for entry in sdw.ordered_entries(existing, refreshed)]
assert refreshed_order == [
"dev",
"v1.4.2",
"v1.4.1",
"v1.4.0",
"v0.0.116",
"legacy",
]
def test_prefix_navigation_paths() -> None:
nav: dict[str, object] = {
"navigation": [
{"page": "Intro", "path": "intro.mdx"},
{
"section": "Guide",
"folder": "guide",
"contents": [{"path": "guide/a.mdx"}],
},
{"page": "External", "path": "https://example.com"},
]
}
sdw.prefix_navigation_paths(nav, "pages-dev")
navigation = cast("list[dict[str, object]]", nav["navigation"])
guide = navigation[1]
contents = cast("list[dict[str, object]]", guide["contents"])
assert navigation[0]["path"] == "../pages-dev/intro.mdx"
assert guide["folder"] == "../pages-dev/guide"
assert contents[0]["path"] == "../pages-dev/guide/a.mdx"
# Absolute URLs are left untouched.
assert navigation[2]["path"] == "https://example.com"
def _make_source_tree(root: Path) -> None:
docs = root / "docs"
docs.mkdir(parents=True)
(docs / "intro.mdx").write_text("# Intro\n", encoding="utf-8")
(docs / "index.yml").write_text(
yaml.safe_dump({"navigation": [{"page": "Intro", "path": "intro.mdx"}]}),
encoding="utf-8",
)
fern = root / "fern"
fern.mkdir(parents=True)
(fern / "docs.yml").write_text(yaml.safe_dump({"versions": []}), encoding="utf-8")
(fern / "assets").mkdir(parents=True)
(fern / "assets" / "logo.svg").write_text("<svg/>", encoding="utf-8")
(fern / "components").mkdir(parents=True)
(fern / "components" / "Card.tsx").write_text(
"export const Card = 1;\n", encoding="utf-8"
)
(fern / "main.css").write_text("body{}\n", encoding="utf-8")
(fern / "fern.config.json").write_text('{"version": "0.0.0"}\n', encoding="utf-8")
def _make_docs_website_tree(root: Path) -> None:
fern = root / "fern"
fern.mkdir(parents=True)
(fern / "docs.yml").write_text(yaml.safe_dump({"versions": []}), encoding="utf-8")
def test_sync_docs_scopes_version_announcements_to_updated_channel(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
(source / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Latest",
"path": "../docs/index.yml",
"slug": "latest",
"announcement": {
"message": "Version 0.0.116 is the final alpha release."
},
}
]
}
),
encoding="utf-8",
)
(website / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"announcement": {"message": "Stale global announcement."},
"versions": [],
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
operation="sync",
source_root=source,
docs_website_root=website,
channel="latest",
source_ref="release-sha",
source_sha="release-sha",
release_version="0.0.116",
version_slug="",
display_name="Latest (v0.0.116)",
availability="",
)
)
(source / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Dev",
"path": "../docs/index.yml",
"slug": "dev",
"announcement": {"message": "OpenShell 0.1.0 is coming soon."},
}
]
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
operation="sync",
source_root=source,
docs_website_root=website,
channel="dev",
source_ref="main",
source_sha="dev-sha",
release_version="0.0.117.dev56",
version_slug="",
display_name="Dev",
availability="",
)
)
fern = website / "fern"
assert (fern / "pages-dev" / "intro.mdx").is_file()
assert (fern / "assets" / "logo.svg").is_file()
version_nav = read_yaml(fern / "versions" / "dev.yml")
assert version_nav["navigation"][0]["path"] == "../pages-dev/intro.mdx"
docs_yml = read_yaml(fern / "docs.yml")
assert docs_yml["versions"] == [
{
"display-name": "Latest (v0.0.116)",
"path": "./versions/latest.yml",
"slug": "latest",
"announcement": {"message": "Version 0.0.116 is the final alpha release."},
},
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
"announcement": {"message": "OpenShell 0.1.0 is coming soon."},
},
]
assert "announcement" not in docs_yml
assert "./components" in docs_yml["experimental"]["mdx-components"]
(source / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Dev",
"path": "../docs/index.yml",
"slug": "dev",
"announcement": {"message": "OpenShell 0.1.0 is released."},
}
]
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
operation="sync",
source_root=source,
docs_website_root=website,
channel="latest",
source_ref="release-0.1.0-sha",
source_sha="release-0.1.0-sha",
release_version="0.1.0",
version_slug="",
display_name="Latest (v0.1.0)",
availability="",
)
)
versions = read_yaml(fern / "docs.yml")["versions"]
assert versions[0]["announcement"] == {"message": "OpenShell 0.1.0 is released."}
assert versions[1]["announcement"] == {"message": "OpenShell 0.1.0 is coming soon."}
def test_sync_docs_clears_updated_badge_and_preserves_other_badges(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
docs_yml_path = website / "fern" / "docs.yml"
docs_yml_path.write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
"availability": "beta",
},
{
"display-name": "v0.0.36",
"path": "./versions/v0.0.36.yml",
"slug": "v0.0.36",
"availability": "deprecated",
},
]
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
operation="sync",
source_root=source,
docs_website_root=website,
channel="dev",
source_ref="main",
source_sha="dev-sha",
release_version="0.0.117.dev56",
version_slug="",
display_name="Dev (v0.0.117.dev56)",
availability="",
)
)
versions = read_yaml(docs_yml_path)["versions"]
assert versions == [
{
"display-name": "Dev (v0.0.117.dev56)",
"path": "./versions/dev.yml",
"slug": "dev",
},
{
"display-name": "v0.0.36",
"path": "./versions/v0.0.36.yml",
"slug": "v0.0.36",
"availability": "deprecated",
},
]
def test_latest_sync_updates_legacy_snapshot_announcement(tmp_path: Path) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
(source / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Latest",
"path": "../docs/index.yml",
"slug": "latest",
"announcement": {
"message": "Version 0.0.116 is the final alpha release."
},
}
]
}
),
encoding="utf-8",
)
docs_yml_path = website / "fern" / "docs.yml"
docs_yml_path.write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Latest (v0.0.116)",
"path": "./versions/latest.yml",
"slug": "latest",
}
]
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
operation="sync",
source_root=source,
docs_website_root=website,
channel="latest",
source_ref="docs/v0.0.116-announcement",
source_sha="announcement-sha",
release_version="0.0.116",
version_slug="",
display_name="Latest (v0.0.116)",
availability="",
)
)
latest = read_yaml(docs_yml_path)["versions"][0]
assert latest["announcement"] == {
"message": "Version 0.0.116 is the final alpha release."
}
snapshots = read_yaml(website / "fern" / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
assert snapshots["latest"] == {
"source-ref": "docs/v0.0.116-announcement",
"source-sha": "announcement-sha",
"version": "0.0.116",
}
def test_stable_sync_creates_immutable_version_and_promotes_latest(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
(website / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Latest (v0.0.116)",
"path": "./versions/latest.yml",
"slug": "latest",
},
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
},
{
"display-name": "v0.0.116",
"path": "./versions/v0.0.116.yml",
"slug": "v0.0.116",
},
]
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
source_root=source,
docs_website_root=website,
channel="stable",
source_ref="v0.2.0",
source_sha="new-sha",
release_version="0.2.0",
version_slug="v0.2.0",
display_name="",
availability="",
allow_rollback=False,
)
)
fern = website / "fern"
assert (fern / "pages-v0.2.0" / "intro.mdx").is_file()
assert (fern / "pages-latest" / "intro.mdx").is_file()
versions = read_yaml(fern / "docs.yml")["versions"]
assert [entry["slug"] for entry in versions] == [
"latest",
"dev",
"v0.2.0",
"v0.0.116",
]
assert versions[0]["display-name"] == "Latest (v0.2.0)"
assert all("availability" not in entry for entry in versions)
snapshots = read_yaml(fern / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
assert snapshots["latest"] == {
"source-ref": "v0.2.0",
"source-sha": "new-sha",
"version": "0.2.0",
}
assert snapshots["v0.2.0"] == {
"source-ref": "v0.2.0",
"source-sha": "new-sha",
"version": "0.2.0",
}
def test_n_minus_one_sync_does_not_move_latest_backwards(tmp_path: Path) -> None:
current = tmp_path / "current"
maintenance = tmp_path / "maintenance"
website = tmp_path / "docs-website"
_make_source_tree(current)
_make_source_tree(maintenance)
(current / "docs" / "intro.mdx").write_text("# Current\n", encoding="utf-8")
(maintenance / "docs" / "intro.mdx").write_text("# Maintenance\n", encoding="utf-8")
_make_docs_website_tree(website)
for source, source_sha, version in (
(current, "current-sha", "0.3.1"),
(maintenance, "maintenance-sha", "0.2.7"),
):
sdw.sync_docs(
Namespace(
source_root=source,
docs_website_root=website,
channel="stable",
source_ref=f"v{version}",
source_sha=source_sha,
release_version=version,
version_slug=f"v{version}",
display_name="",
availability="",
allow_rollback=False,
)
)
fern = website / "fern"
assert (fern / "pages-latest" / "intro.mdx").read_text(
encoding="utf-8"
) == "# Current\n"
assert (fern / "pages-v0.2.7" / "intro.mdx").read_text(
encoding="utf-8"
) == "# Maintenance\n"
snapshots = read_yaml(fern / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
assert snapshots["latest"] == {
"source-ref": "v0.3.1",
"source-sha": "current-sha",
"version": "0.3.1",
}
def test_stable_sync_preserves_newer_legacy_latest_without_metadata(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
fern = website / "fern"
(fern / "pages-latest").mkdir()
(fern / "pages-latest" / "intro.mdx").write_text(
"# Existing latest\n", encoding="utf-8"
)
(fern / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Latest (v0.3.1)",
"path": "./versions/latest.yml",
"slug": "latest",
}
]
}
),
encoding="utf-8",
)
sdw.sync_docs(
Namespace(
source_root=source,
docs_website_root=website,
channel="stable",
source_ref="v0.2.7",
source_sha="maintenance-sha",
release_version="0.2.7",
version_slug="v0.2.7",
display_name="",
availability="",
allow_rollback=False,
)
)
assert (fern / "pages-latest" / "intro.mdx").read_text(
encoding="utf-8"
) == "# Existing latest\n"
snapshots = read_yaml(fern / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
assert snapshots["latest"] == {
"source-ref": "",
"source-sha": "",
"version": "0.3.1",
}
def test_dev_sync_rejects_stale_or_conflicting_updates(tmp_path: Path) -> None:
current = tmp_path / "current"
stale = tmp_path / "stale"
website = tmp_path / "docs-website"
_make_source_tree(current)
_make_source_tree(stale)
(current / "docs" / "intro.mdx").write_text("# Current\n", encoding="utf-8")
(stale / "docs" / "intro.mdx").write_text("# Stale\n", encoding="utf-8")
_make_docs_website_tree(website)
def sync(source: Path, source_sha: str, version: str) -> None:
sdw.sync_docs(
Namespace(
source_root=source,
docs_website_root=website,
channel="dev",
source_ref="main",
source_sha=source_sha,
release_version=version,
version_slug="",
display_name=f"Dev (v{version})",
availability="beta",
allow_rollback=False,
)
)
sync(current, "current-sha", "0.3.2.dev10")
sync(stale, "stale-sha", "0.3.2.dev9")
intro = website / "fern" / "pages-dev" / "intro.mdx"
assert intro.read_text(encoding="utf-8") == "# Current\n"
with pytest.raises(ValueError, match="already points to current-sha"):
sync(stale, "other-sha", "0.3.2.dev10")
def test_dev_sync_allows_explicit_rollback(tmp_path: Path) -> None:
current = tmp_path / "current"
stale = tmp_path / "stale"
website = tmp_path / "docs-website"
_make_source_tree(current)
_make_source_tree(stale)
(current / "docs" / "intro.mdx").write_text("# Current\n", encoding="utf-8")
(stale / "docs" / "intro.mdx").write_text("# Rolled back\n", encoding="utf-8")
_make_docs_website_tree(website)
def sync(source: Path, source_sha: str, version: str, allow_rollback: bool) -> None:
sdw.sync_docs(
Namespace(
source_root=source,
docs_website_root=website,
channel="dev",
source_ref="main",
source_sha=source_sha,
release_version=version,
version_slug="",
display_name=f"Dev (v{version})",
availability="beta",
allow_rollback=allow_rollback,
)
)
sync(current, "current-sha", "0.3.2.dev10", False)
sync(stale, "rollback-sha", "0.3.2.dev9", True)
fern = website / "fern"
assert (fern / "pages-dev" / "intro.mdx").read_text(
encoding="utf-8"
) == "# Rolled back\n"
snapshots = read_yaml(fern / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
assert snapshots["dev"] == {
"source-ref": "main",
"source-sha": "rollback-sha",
"version": "0.3.2.dev9",
}
def test_immutable_snapshot_cannot_change_source(tmp_path: Path) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
args = Namespace(
source_root=source,
docs_website_root=website,
channel="stable",
source_ref="main",
source_sha="release-sha",
release_version="0.2.0",
version_slug="v0.2.0",
display_name="",
availability="",
allow_rollback=False,
)
sdw.sync_docs(args)
args.source_sha = "different-sha"
with pytest.raises(ValueError, match=r"immutable snapshot v0\.2\.0"):
sdw.sync_docs(args)
def test_only_dev_refreshes_shared_fern_files(tmp_path: Path) -> None:
dev = tmp_path / "dev"
release = tmp_path / "release"
website = tmp_path / "docs-website"
_make_source_tree(dev)
_make_source_tree(release)
(dev / "fern" / "components" / "Card.tsx").write_text(
"export const Card = 'dev';\n", encoding="utf-8"
)
(release / "fern" / "components" / "Card.tsx").write_text(
"export const Card = 'release';\n", encoding="utf-8"
)
_make_docs_website_tree(website)
sdw.sync_docs(
Namespace(
source_root=dev,
docs_website_root=website,
channel="dev",
source_ref="main",
source_sha="dev-sha",
release_version="0.2.1.dev1",
version_slug="",
display_name="Dev (v0.2.1.dev1)",
availability="beta",
allow_rollback=False,
)
)
sdw.sync_docs(
Namespace(
source_root=release,
docs_website_root=website,
channel="stable",
source_ref="v0.2.0",
source_sha="release-sha",
release_version="0.2.0",
version_slug="v0.2.0",
display_name="",
availability="",
allow_rollback=False,
)
)
card = website / "fern" / "components" / "Card.tsx"
assert card.read_text(encoding="utf-8") == "export const Card = 'dev';\n"
def test_remove_docs_drops_snapshot(tmp_path: Path) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
base = Namespace(
operation="sync",
source_root=source,
docs_website_root=website,
channel="version",
source_ref="v0.0.36",
source_sha="release-sha",
version_slug="v0.0.36",
display_name="",
availability="deprecated",
)
sdw.sync_docs(base)
fern = website / "fern"
assert (fern / "pages-v0.0.36").is_dir()
assert (fern / "versions" / "v0.0.36.yml").is_file()
assert read_yaml(fern / "docs.yml")["versions"][0]["availability"] == "deprecated"
sdw.remove_docs(
Namespace(
operation="remove",
source_root=None,
docs_website_root=website,
channel="version",
source_ref="",
version_slug="v0.0.36",
display_name="",
availability="",
)
)
assert not (fern / "pages-v0.0.36").exists()
assert not (fern / "versions" / "v0.0.36.yml").exists()
docs_yml = read_yaml(fern / "docs.yml")
assert [entry["slug"] for entry in docs_yml["versions"]] == []
@pytest.mark.parametrize("channel", ["stable", "version"])
def test_immutable_snapshot_uses_resolved_commit_identity(
tmp_path: Path, channel: str
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
args = Namespace(
source_root=source,
docs_website_root=website,
channel=channel,
source_ref="main",
source_sha="first-sha",
release_version="0.2.0" if channel == "stable" else "",
version_slug="v0.2.0",
display_name="",
availability="",
allow_rollback=False,
)
sdw.sync_docs(args)
(source / "docs" / "intro.mdx").write_text("# Changed\n", encoding="utf-8")
args.source_sha = "second-sha"
with pytest.raises(ValueError, match=r"immutable snapshot v0\.2\.0"):
sdw.sync_docs(args)
assert (website / "fern" / "pages-v0.2.0" / "intro.mdx").read_text(
encoding="utf-8"
) == "# Intro\n"
def test_stable_promotion_replaces_latest_page_components(tmp_path: Path) -> None:
old = tmp_path / "old"
new = tmp_path / "new"
website = tmp_path / "docs-website"
_make_source_tree(old)
_make_source_tree(new)
(old / "docs" / "_components").mkdir()
(old / "docs" / "_components" / "Widget.tsx").write_text(
"export const Widget = 'old';\n", encoding="utf-8"
)
(new / "docs" / "_components").mkdir()
(new / "docs" / "_components" / "Widget.tsx").write_text(
"export const Widget = 'new';\n", encoding="utf-8"
)
_make_docs_website_tree(website)
for source, source_sha, version in (
(old, "old-sha", "1.0.0"),
(new, "new-sha", "1.1.0"),
):
sdw.sync_docs(
Namespace(
source_root=source,
docs_website_root=website,
channel="stable",
source_ref=f"v{version}",
source_sha=source_sha,
release_version=version,
version_slug=f"v{version}",
display_name="",
availability="",
allow_rollback=False,
)
)
widget = website / "fern" / "pages-latest" / "_components" / "Widget.tsx"
assert widget.read_text(encoding="utf-8") == "export const Widget = 'new';\n"
@pytest.mark.parametrize("channel", ["latest", "stable"])
def test_sync_replaces_latest_redirects_with_snapshot(
tmp_path: Path, channel: str
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
source_config = source / "fern" / "docs.yml"
target_config = website / "fern" / "docs.yml"
aliases = [
{
"source": "/openshell/tutorials",
"destination": "/openshell/latest/tutorials",
},
{
"source": "/openshell/tutorials/:path*",
"destination": "/openshell/latest/tutorials/:path*",
},
]
preserved = [
# Source ownership wins over the destination channel.
{"source": "/openshell/dev/retired", "destination": "/openshell/latest"},
{"source": "/openshell/v0.0.116/old", "destination": "/openshell/v0.0.116/new"},
{"source": "/openshell/:path*.html", "destination": "/openshell/:path*"},
]
old_aliases = [
{
"source": rule["source"],
"destination": rule["destination"].replace(
"/latest/tutorials", "/latest/get-started/tutorials"
),
}
for rule in aliases
]
stale = [
{
"source": rule["source"].replace("/openshell/", "/openshell/latest/", 1),
"destination": rule["destination"],
}
for rule in old_aliases
]
sdw.write_yaml(source_config, {"versions": [], "redirects": aliases})
sdw.write_yaml(
target_config,
{
"versions": [
{
"slug": "v0.0.116",
"display-name": "v0.0.116",
"path": "./versions/v0.0.116.yml",
}
],
"redirects": stale + old_aliases + preserved,
},
)
args = Namespace(
source_root=source,
docs_website_root=website,
channel=channel,
source_ref="v0.1.1",
source_sha="release-sha",
release_version="0.1.1",
version_slug="v0.1.1" if channel == "stable" else "",
display_name="",
availability="",
allow_rollback=False,
)
# Repeating the same snapshot can repair routing without changing content.
for _ in range(2):
sdw.sync_docs(args)
assert read_yaml(target_config)["redirects"] == aliases + preserved
assert (website / "fern" / "pages-latest" / "intro.mdx").is_file()
# A maintenance release must not restore the stale redirects.
sdw.write_yaml(source_config, {"versions": [], "redirects": stale + old_aliases})
args.source_ref = "v0.0.117"
args.source_sha = "maintenance-sha"
args.release_version = "0.0.117"
args.version_slug = "v0.0.117" if channel == "stable" else ""
sdw.sync_docs(args)
assert read_yaml(target_config)["redirects"] == aliases + preserved
def test_dev_sync_updates_own_and_shared_redirects_only(tmp_path: Path) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
source_config = source / "fern" / "docs.yml"
target_config = website / "fern" / "docs.yml"
latest = {
"source": "/openshell/latest/index.html",
"destination": "/openshell/latest",
}
dev = {"source": "/openshell/dev/old", "destination": "/openshell/dev/new#section"}
shared = {
"source": "/openshell/:path*/index.html",
"destination": "/openshell/:path*",
}
stale = {
"source": "/openshell/dev/removed",
"destination": "/openshell/dev/deleted",
}
sdw.write_yaml(target_config, {"versions": [], "redirects": [latest, stale]})
sdw.write_yaml(source_config, {"versions": [], "redirects": [dev, shared]})
args = Namespace(
source_root=source,
docs_website_root=website,
channel="dev",
source_ref="main",
source_sha="dev-sha",
release_version="0.2.0.dev1",
version_slug="",
display_name="",
availability="",
allow_rollback=False,
)
sdw.sync_docs(args)
# Keep the explicit latest/index.html rule ahead of the shared wildcard.
assert read_yaml(target_config)["redirects"] == [dev, latest, shared]
# Removing the entire field removes only dev/shared rules.
sdw.write_yaml(source_config, {"versions": []})
sdw.sync_docs(args)
assert read_yaml(target_config)["redirects"] == [latest]