← Files AkinatorARCHIVED FILE

tests/test_libraries.py

22.6 KB · Oct 3, 2026 · 06:33 UTC

↓ Download file

"""Tests for the library wiki - one page per declared dependency.

Akinator's own repository declares no dependencies, so a passing run here proves
nothing about the generator. Per rules/11, every behaviour is exercised against a
scratch repository built to trip it, and every check is shown both firing on the
broken case and staying silent on the healthy one.

The design claims under test:

- a page per dependency, per ecosystem, with facts that are extracted, never
  written by hand;
- usage detection by import, not by substring (`react` is not `react-dom`);
- the curated half of a page is a person's, and survives every regeneration
  byte for byte;
- a dependency that disappears leaves its page flagged, never deleted.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

import akinator_coverage as cov
import extract_libraries as el

WIKI = "docs/wiki/libraries"


def _write(root: Path, rel: str, content: str) -> Path:
    path = root / rel
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8", newline="\n")
    return path


def _page(root: Path, slug: str) -> Path:
    return root / WIKI / f"{slug}.md"


def _block(text: str) -> str:
    start = text.index(el.BEGIN)
    return text[start:text.index(el.END, start) + len(el.END)]


def _outside(text: str) -> tuple[str, str]:
    start = text.index(el.BEGIN)
    stop = text.index(el.END, start) + len(el.END)
    return text[:start], text[stop:]


def _polyglot(root: Path) -> None:
    """A repository with one dependency per ecosystem, each imported once."""
    _write(root, "package.json", json.dumps({
        "dependencies": {"react": "^18.2.0", "react-dom": "^18.2.0"},
        "devDependencies": {"vitest": "~1.2.0"},
    }))
    _write(root, "requirements.txt", "requests==2.31.0\n")
    _write(root, "go.mod", (
        "module example.com/app\n\ngo 1.22\n\n"
        "require (\n\tgithub.com/gin-gonic/gin v1.9.1\n)\n"
    ))
    _write(root, "Cargo.toml", '[dependencies]\nserde = "1.0"\n')
    _write(root, "web/App.tsx", "import React from 'react';\n")
    _write(root, "api/client.py", "import requests\n")
    _write(root, "server/main.go",
           'package main\n\nimport (\n\t"fmt"\n\t"github.com/gin-gonic/gin"\n)\n')
    _write(root, "src/lib.rs", "use serde::Serialize;\n")


# --------------------------------------------------------------------------
# Pages, one per dependency, per ecosystem
# --------------------------------------------------------------------------

def test_pages_are_created_per_ecosystem(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    assert el.main([str(tmp_path), "--write"]) == 0

    for slug, title, ecosystem in (
        ("react", "react", "npm"),
        ("requests", "requests", "python"),
        ("github.com-gin-gonic-gin", "github.com/gin-gonic/gin", "go"),
        ("serde", "serde", "rust"),
    ):
        page = _page(tmp_path, slug)
        assert page.is_file(), f"no page for {title}"
        text = page.read_text(encoding="utf-8")
        assert text.startswith(f"# {title}\n"), text[:60]
        block = _block(text)
        assert f"**Ecosystem:** {ecosystem}" in block
        assert "Regenerate with: `python <skill>/scripts/extract_libraries.py --write`" in block


def test_a_new_page_carries_every_curated_section_as_an_honest_gap(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    text = _page(tmp_path, "react").read_text(encoding="utf-8")
    _before, after = _outside(text)
    for section in el.CURATED_SECTIONS:
        assert f"## {section}\n\n{el.GAP}\n" in after, section
    lines = text.splitlines()
    assert lines.count(el.GAP) == len(el.CURATED_SECTIONS), (
        "the gap marker must be the exact line, once per section, so tools can count it"
    )


def test_the_generated_block_states_the_facts(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    block = _block(_page(tmp_path, "react").read_text(encoding="utf-8"))
    assert "**Version:** `^18.2.0`" in block
    assert "**Kind:** runtime" in block
    assert "**Declared in:** `package.json`" in block
    assert "**Used in:** 1 file" in block
    assert "  - `web/App.tsx`" in block

    dev = _block(_page(tmp_path, "vitest").read_text(encoding="utf-8"))
    assert "**Kind:** dev" in dev
    assert "**Used in:** 0 files" in dev, "no import must be stated, not hidden"


def test_the_index_lists_every_library(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    index = (tmp_path / WIKI / "README.md").read_text(encoding="utf-8")
    assert "| Library | Ecosystem | Version | Kind | Used in | Page |" in index
    assert "| react | npm | `^18.2.0` | runtime | 1 file | [react.md](react.md) |" in index
    assert "| vitest | npm | `~1.2.0` | dev | 0 files | [vitest.md](vitest.md) |" in index
    assert "[github.com-gin-gonic-gin.md](github.com-gin-gonic-gin.md)" in index
    assert "## Libraries (6)" in index


def test_a_dependency_declared_twice_is_one_page_with_both_versions(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "^1.6.0"}}))
    _write(tmp_path, "legacy/package.json", json.dumps({"dependencies": {"axios": "^0.27.0"}}))
    el.main([str(tmp_path), "--write"])
    block = _block(_page(tmp_path, "axios").read_text(encoding="utf-8"))
    assert "`^0.27.0` in `legacy/package.json`" in block
    assert "`^1.6.0` in `package.json`" in block, "version drift across modules is a fact"


def test_a_dependency_links_to_the_decision_that_chose_it(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    _write(tmp_path, "docs/adr/0009-http-client.md", "# ADR 0009 - axios over fetch\n")
    el.main([str(tmp_path), "--write"])
    block = _block(_page(tmp_path, "axios").read_text(encoding="utf-8"))
    assert "**Decisions and incidents:** `docs/adr/0009-http-client.md`" in block


def test_long_usage_lists_are_capped(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"lodash": "4"}}))
    for n in range(30):
        _write(tmp_path, f"src/m{n:02d}.js", "const _ = require('lodash');\n")
    el.main([str(tmp_path), "--write"])
    block = _block(_page(tmp_path, "lodash").read_text(encoding="utf-8"))
    assert "**Used in:** 30 files" in block
    assert block.count("  - `src/") == el.MAX_LISTED
    assert "  - ... and 5 more" in block
    assert "`src/m00.js`" in block and "`src/m25.js`" not in block, "first 25, sorted"


# --------------------------------------------------------------------------
# Usage detection - hits and misses
# --------------------------------------------------------------------------

def _used(root: Path, ecosystem: str, name: str) -> list[str]:
    return el.usage(el.Library(ecosystem, name), el.scan(root))


def test_npm_usage_is_exact_not_a_substring(tmp_path: Path) -> None:
    """`react` must not match an import of `react-dom` - the substring failure."""
    _write(tmp_path, "a.tsx", "import ReactDOM from 'react-dom/client';\n")
    _write(tmp_path, "b.tsx", "import { jsx } from 'react/jsx-runtime';\n")
    _write(tmp_path, "c.cjs", "const React = require(\"react\");\n")
    _write(tmp_path, "d.ts", "const lazy = import('react');\n")
    _write(tmp_path, "e.ts", "import {\n  useState,\n} from 'react';\n")
    _write(tmp_path, "f.js", "import react from './react';\n")
    _write(tmp_path, "g.mjs", "export { x } from 'preact';\n")

    assert _used(tmp_path, "npm", "react") == ["b.tsx", "c.cjs", "d.ts", "e.ts"]
    assert _used(tmp_path, "npm", "react-dom") == ["a.tsx"]


def test_npm_scoped_packages_resolve_to_their_scope(tmp_path: Path) -> None:
    _write(tmp_path, "a.ts", "import { x } from '@tanstack/query-core/build';\n")
    _write(tmp_path, "b.ts", "import 'node:fs';\nimport '@tanstack/react-query';\n")
    assert _used(tmp_path, "npm", "@tanstack/query-core") == ["a.ts"]
    assert _used(tmp_path, "npm", "@tanstack/react-query") == ["b.ts"]


def test_python_usage_maps_distribution_to_import_name(tmp_path: Path) -> None:
    _write(tmp_path, "a.py", "import requests\n")
    _write(tmp_path, "b.py", "from requests.adapters import HTTPAdapter\n")
    _write(tmp_path, "c.py", "import requests_toolbelt\n")
    _write(tmp_path, "d.py", "from .requests import local\n")
    _write(tmp_path, "e.py", "import yaml\n")
    _write(tmp_path, "f.py", "from dateutil import parser\n")
    _write(tmp_path, "g.py", "import os, typing_extensions as te\n")
    _write(tmp_path, "h.py", "from google.auth import default\n")

    assert _used(tmp_path, "python", "requests") == ["a.py", "b.py"]
    assert _used(tmp_path, "python", "PyYAML") == ["e.py"]
    assert _used(tmp_path, "python", "python-dateutil") == ["f.py"]
    assert _used(tmp_path, "python", "typing-extensions") == ["g.py"]
    assert _used(tmp_path, "python", "google-auth") == ["h.py"]
    assert _used(tmp_path, "python", "requests-toolbelt") == ["c.py"]


def test_go_usage_matches_by_import_path_prefix(tmp_path: Path) -> None:
    _write(tmp_path, "a.go", 'package a\n\nimport "github.com/gin-gonic/gin/binding"\n')
    _write(tmp_path, "b.go", 'package b\n\nimport (\n\tg "github.com/gin-gonic/gin"\n)\n')
    _write(tmp_path, "c.go", 'package c\n\nimport "github.com/gin-gonic/ginx"\n')
    assert _used(tmp_path, "go", "github.com/gin-gonic/gin") == ["a.go", "b.go"]


def test_rust_usage_maps_dashes_to_underscores(tmp_path: Path) -> None:
    _write(tmp_path, "a.rs", "use tokio_util::codec::Framed;\n")
    _write(tmp_path, "b.rs", "#[tokio::main]\nasync fn main() {}\n")
    _write(tmp_path, "c.rs", "#[macro_use] extern crate serde_derive;\n")
    _write(tmp_path, "d.rs", "let v = serde_json::to_string(&x);\n")
    assert _used(tmp_path, "rust", "tokio-util") == ["a.rs"]
    assert _used(tmp_path, "rust", "tokio") == ["b.rs"], "tokio is not tokio_util"
    assert _used(tmp_path, "rust", "serde_derive") == ["c.rs"]
    assert _used(tmp_path, "rust", "serde") == [], "serde is not serde_json"


def test_an_ecosystem_without_a_detector_falls_back_to_import_lines(tmp_path: Path) -> None:
    _write(tmp_path, "A.java", "import com.google.guava.collect.Lists;\n")
    _write(tmp_path, "B.java", "// guava is great\nclass B {}\n")
    _write(tmp_path, "C.java", "import com.google.guava-extra.X;\n")
    assert _used(tmp_path, "maven", "guava") == ["A.java"]


def test_vendored_and_generated_trees_are_not_scanned(tmp_path: Path) -> None:
    _write(tmp_path, "node_modules/x/index.js", "require('react');\n")
    _write(tmp_path, "dist/bundle.js", "require('react');\n")
    _write(tmp_path, ".git/hooks/x.js", "require('react');\n")
    _write(tmp_path, "src/a.js", "require('react');\n")
    assert _used(tmp_path, "npm", "react") == ["src/a.js"]


# --------------------------------------------------------------------------
# The curated half belongs to a person
# --------------------------------------------------------------------------

def test_curated_text_survives_regeneration_byte_for_byte(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    page = _page(tmp_path, "react")
    text = page.read_text(encoding="utf-8")
    curated = text.replace(
        f"## Why this library\n\n{el.GAP}",
        "## Why this library\n\nChosen over Vue in 2023 - see the hiring plan.",
    ).replace("# react\n", "# react\n\nOwner: web team.\n")
    page.write_text(curated, encoding="utf-8", newline="\n")
    before, after = _outside(curated)

    _write(tmp_path, "package.json", json.dumps({
        "dependencies": {"react": "^19.0.0", "react-dom": "^18.2.0"},
        "devDependencies": {"vitest": "~1.2.0"},
    }))
    assert el.main([str(tmp_path), "--write"]) == 0

    regenerated = page.read_text(encoding="utf-8")
    assert _outside(regenerated) == (before, after), "only the block may change"
    assert "**Version:** `^19.0.0`" in _block(regenerated)
    assert "Chosen over Vue in 2023" in regenerated


def test_crlf_pages_keep_their_bytes_outside_the_block(tmp_path: Path) -> None:
    """A page edited on Windows keeps its line endings - read as bytes, not text."""
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    el.main([str(tmp_path), "--write"])
    page = _page(tmp_path, "axios")
    raw = page.read_bytes().replace(b"\n", b"\r\n")
    page.write_bytes(raw)
    assert el.main([str(tmp_path), "--check"]) == 0, "line endings alone are not drift"

    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "2"}}))
    el.main([str(tmp_path), "--write"])
    new = page.read_bytes()
    old_text, new_text = raw.decode("utf-8"), new.decode("utf-8")
    assert _outside(new_text) == _outside(old_text)
    assert "\n" not in new_text.replace("\r\n", ""), "no bare LF introduced"
    assert "**Version:** `2`" in new_text


def test_a_hand_written_page_without_markers_gets_the_block_inserted(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    human = "# axios\n\nWe wrap it in src/http.ts.\n"
    _write(tmp_path, f"{WIKI}/axios.md", human)
    el.main([str(tmp_path), "--write"])
    text = _page(tmp_path, "axios").read_text(encoding="utf-8")
    before, after = _outside(text)
    assert before == "# axios\n\n"
    assert after == "\n\nWe wrap it in src/http.ts.\n", "the human text is untouched"
    assert el.main([str(tmp_path), "--check"]) == 0, "insertion must be idempotent"


def test_a_broken_block_is_refused_not_guessed(tmp_path: Path) -> None:
    """A begin marker with no end: replacing to end-of-file would eat the curated
    text, so the tool refuses and says so."""
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    broken = f"# axios\n\n{el.BEGIN}\n- stale\n\n## Why this library\n\nKept.\n"
    page = _write(tmp_path, f"{WIKI}/axios.md", broken)
    assert el.main([str(tmp_path), "--write"]) == 2
    assert page.read_text(encoding="utf-8") == broken, "the page must be untouched"
    assert el.main([str(tmp_path), "--check"]) == 1


# --------------------------------------------------------------------------
# Drift: --check fires on drift and is silent when current
# --------------------------------------------------------------------------

def test_check_is_silent_when_current(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    assert el.main([str(tmp_path), "--check"]) == 0
    assert el.main([str(tmp_path)]) == 0, "a current tree is not a dry-run change"
    changes, errors = el.plan(tmp_path)
    assert changes == {} and errors == []


def test_check_fires_on_a_stale_generated_block(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    _write(tmp_path, "requirements.txt", "requests==2.32.0\n")
    assert el.main([str(tmp_path), "--check"]) == 1


def test_check_fires_on_a_hand_edited_block(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    page = _page(tmp_path, "requests")
    page.write_text(page.read_text(encoding="utf-8").replace(
        "**Ecosystem:** python", "**Ecosystem:** pip"), encoding="utf-8", newline="\n")
    assert el.main([str(tmp_path), "--check"]) == 1


def test_check_fires_on_a_missing_page(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    _page(tmp_path, "serde").unlink()
    assert el.main([str(tmp_path), "--check"]) == 1


def test_check_fires_on_a_new_import(tmp_path: Path) -> None:
    """Usage is part of the facts - a new importer is drift too."""
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    _write(tmp_path, "web/Other.tsx", "import { useState } from 'react';\n")
    assert el.main([str(tmp_path), "--check"]) == 1


def test_check_fires_on_a_stale_index(tmp_path: Path) -> None:
    _polyglot(tmp_path)
    el.main([str(tmp_path), "--write"])
    index = tmp_path / WIKI / "README.md"
    index.write_text(index.read_text(encoding="utf-8").replace("| serde |", "| serde2 |"),
                     encoding="utf-8", newline="\n")
    assert el.main([str(tmp_path), "--check"]) == 1


def test_dry_run_reports_and_writes_nothing(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
    _polyglot(tmp_path)
    assert el.main([str(tmp_path)]) == 1
    assert not (tmp_path / WIKI).exists(), "a dry run must not write"
    out = capsys.readouterr().out
    assert f"missing: {WIKI}/react.md" in out
    assert "Fix with: python <skill>/scripts/extract_libraries.py --write" in out


def test_custom_directory(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    assert el.main([str(tmp_path), "--write", "--dir", "wiki/deps"]) == 0
    assert (tmp_path / "wiki/deps/axios.md").is_file()
    assert (tmp_path / "wiki/deps/README.md").is_file()
    assert el.main([str(tmp_path), "--check", "--dir", "wiki/deps"]) == 0


# --------------------------------------------------------------------------
# A dependency that disappears
# --------------------------------------------------------------------------

def test_a_removed_dependency_is_flagged_never_deleted(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1", "left-pad": "1"}}))
    el.main([str(tmp_path), "--write"])
    page = _page(tmp_path, "left-pad")
    page.write_text(page.read_text(encoding="utf-8").replace(
        f"## Pitfalls and incidents\n\n{el.GAP}",
        "## Pitfalls and incidents\n\nUnpublished upstream in 2016; build broke."),
        encoding="utf-8", newline="\n")

    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    assert el.main([str(tmp_path), "--check"]) == 1, "a removal is drift"
    assert el.main([str(tmp_path), "--write"]) == 0

    assert page.is_file(), "the page may hold the only record of why - never delete"
    text = page.read_text(encoding="utf-8")
    assert "Unpublished upstream in 2016" in text
    assert "no longer declared" in _block(text)
    assert "**Ecosystem:**" not in _block(text), "stale facts must not survive"

    index = (tmp_path / WIKI / "README.md").read_text(encoding="utf-8")
    assert "- [left-pad](left-pad.md) - no longer declared - review or delete" in index
    assert "| left-pad |" not in index
    assert el.main([str(tmp_path), "--check"]) == 0


def test_a_hand_written_page_for_an_undeclared_library_is_listed_not_touched(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {"axios": "1"}}))
    human = "# libcurl\n\nLinked by the system image.\n"
    page = _write(tmp_path, f"{WIKI}/libcurl.md", human)
    el.main([str(tmp_path), "--write"])
    assert page.read_text(encoding="utf-8") == human
    index = (tmp_path / WIKI / "README.md").read_text(encoding="utf-8")
    assert "- [libcurl](libcurl.md) - no longer declared - review or delete" in index


# --------------------------------------------------------------------------
# Slugs
# --------------------------------------------------------------------------

def test_slugs_are_filesystem_safe() -> None:
    assert el.slugify("@types/node") == "types-node"
    assert el.slugify("github.com/gin-gonic/gin") == "github.com-gin-gonic-gin"
    assert el.slugify("Flask") == "flask"


def test_slug_collisions_are_disambiguated(tmp_path: Path) -> None:
    _write(tmp_path, "package.json", json.dumps({"dependencies": {
        "yaml": "2", "@foo/bar": "1", "foo-bar": "1", "readme": "1"}}))
    _write(tmp_path, "requirements.txt", "yaml\n")
    el.main([str(tmp_path), "--write"])
    names = sorted(p.name for p in (tmp_path / WIKI).glob("*.md"))
    assert "yaml-npm.md" in names and "yaml-python.md" in names
    assert "foo-bar.md" in names and "foo-bar-2.md" in names
    assert "# foo-bar\n" in (tmp_path / WIKI / "foo-bar.md").read_text(encoding="utf-8"), (
        "the name that already is its slug keeps the plain page"
    )
    assert "readme-npm.md" in names, "a library named readme must not overwrite the index"
    index = (tmp_path / WIKI / "README.md").read_text(encoding="utf-8")
    assert index.startswith("# Libraries\n")
    assert el.main([str(tmp_path), "--check"]) == 0


# --------------------------------------------------------------------------
# Generation contract
# --------------------------------------------------------------------------

def test_output_is_deterministic_and_names_no_absolute_path(tmp_path: Path) -> None:
    first, second = tmp_path / "one", tmp_path / "two"
    for root in (first, second):
        _polyglot(root)
    # Build the second tree in a different order: it must not matter.
    (second / "src/lib.rs").unlink()
    _write(second, "src/lib.rs", "use serde::Serialize;\n")

    plan_one, _ = el.plan(first)
    plan_two, _ = el.plan(second)
    rel_one = {p.relative_to(first).as_posix(): c for p, c in plan_one.items()}
    rel_two = {p.relative_to(second).as_posix(): c for p, c in plan_two.items()}
    assert rel_one == rel_two
    assert el.plan(first)[0] == plan_one, "same tree, same output"
    for content in rel_one.values():
        assert str(first) not in content and first.as_posix() not in content


def test_an_empty_repository_gets_an_explicit_index(tmp_path: Path) -> None:
    """Standard-library only is a fact, so it is written down, not left blank."""
    _write(tmp_path, "main.py", "import os\n")
    assert el.main([str(tmp_path), "--check"]) == 1, "a missing index is drift"
    assert el.main([str(tmp_path), "--write"]) == 0
    index = (tmp_path / WIKI / "README.md").read_text(encoding="utf-8")
    assert "## Libraries (0)" in index
    assert "standard-library only" in index
    assert sorted(p.name for p in (tmp_path / WIKI).iterdir()) == ["README.md"]
    assert el.main([str(tmp_path), "--check"]) == 0


def test_not_a_directory_is_an_error(tmp_path: Path) -> None:
    assert el.main([str(tmp_path / "absent")]) == 2


def test_the_generated_wiki_passes_the_coverage_checker(tmp_path: Path) -> None:
    """Checked from the destination (rules/12): the pages land in a host repo,
    so they must not hand that repo a single finding - no dead link, no path
    that is not there, no generated banner naming a generator it lacks."""
    _polyglot(tmp_path)
    _write(tmp_path, "go.mod", (
        "module example.com/app\n\ngo 1.22\n\n"
        "require (\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgopkg.in/yaml.v3 v3.0.1\n)\n"
    ))
    el.main([str(tmp_path), "--write"])
    findings = cov.run_checks(cov.Repo(tmp_path), only=(), skip=(), sample=0)
    ours = [f for f in findings if f.path.startswith(WIKI)]
    assert ours == [], [(f.check, f.path, f.message) for f in ours]


def test_this_repo_declares_no_libraries(repo: Path) -> None:
    """Akinator is stdlib-only, so its own wiki would be the explicit empty index."""
    assert el.libraries(repo) == []

SHA-256: 44c2bef5277b9133c9344d188475db695ca7ebbc87b5f23b58d6d803de619526