wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation
This commit is contained in:
134
.agent/knowledge/awesome_claude/tests/conftest.py
Normal file
134
.agent/knowledge/awesome_claude/tests/conftest.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Shared pytest fixtures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class DummyReset:
|
||||
"""Minimal reset object with a timestamp."""
|
||||
|
||||
@staticmethod
|
||||
def timestamp() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
class DummyCore:
|
||||
"""Minimal core rate limit object."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.remaining = 5000
|
||||
self.limit = 5000
|
||||
self.reset = DummyReset()
|
||||
|
||||
|
||||
class DummyRateLimit:
|
||||
"""Minimal rate limit response wrapper."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.core = DummyCore()
|
||||
|
||||
|
||||
class DummyIssue:
|
||||
"""Minimal issue object with an html_url."""
|
||||
|
||||
html_url = "https://example.com/issue"
|
||||
|
||||
|
||||
class DummyRepo:
|
||||
"""Minimal GitHub repo stub for notifications."""
|
||||
|
||||
def __init__(self, full_name: str) -> None:
|
||||
self.full_name = full_name
|
||||
|
||||
def get_label(self, name: str):
|
||||
return object()
|
||||
|
||||
def create_label(self, name: str, color: str, description: str):
|
||||
return object()
|
||||
|
||||
def get_issues(self, **kwargs):
|
||||
return []
|
||||
|
||||
def create_issue(self, title: str, body: str, labels: list[str]):
|
||||
return DummyIssue()
|
||||
|
||||
|
||||
class DummyUser:
|
||||
"""Minimal GitHub user object."""
|
||||
|
||||
login = "dummy-user"
|
||||
|
||||
|
||||
class DummyGithub:
|
||||
"""Minimal GitHub client stub."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def get_rate_limit(self):
|
||||
return DummyRateLimit()
|
||||
|
||||
def get_repo(self, full_name: str):
|
||||
return DummyRepo(full_name)
|
||||
|
||||
def get_user(self):
|
||||
return DummyUser()
|
||||
|
||||
|
||||
def find_repo_root(start: Path) -> Path:
|
||||
p = start.resolve()
|
||||
while not (p / "pyproject.toml").exists():
|
||||
if p.parent == p:
|
||||
raise RuntimeError("Repo root not found")
|
||||
p = p.parent
|
||||
return p
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def repo_root() -> Path:
|
||||
"""Resolve the repository root for tests."""
|
||||
return find_repo_root(Path(__file__))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def github_stub(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Replace the GitHub client to avoid network calls."""
|
||||
try:
|
||||
import scripts.utils.github_utils as github_utils
|
||||
|
||||
monkeypatch.setattr(github_utils, "Github", DummyGithub)
|
||||
monkeypatch.setattr(github_utils, "_GITHUB_CLIENTS", {})
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
modules = []
|
||||
for name in (
|
||||
"badge_notification_core",
|
||||
"scripts.badge_notification_core",
|
||||
"scripts.badges.badge_notification_core",
|
||||
):
|
||||
if name in sys.modules:
|
||||
modules.append(sys.modules[name])
|
||||
|
||||
if not modules:
|
||||
for name in (
|
||||
"badge_notification_core",
|
||||
"scripts.badge_notification_core",
|
||||
"scripts.badges.badge_notification_core",
|
||||
):
|
||||
try:
|
||||
modules.append(importlib.import_module(name))
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
if not modules:
|
||||
raise RuntimeError("Could not locate badge_notification_core module for stubbing")
|
||||
|
||||
for module in modules:
|
||||
monkeypatch.setattr(module, "Github", DummyGithub)
|
||||
return DummyGithub
|
||||
40
.agent/knowledge/awesome_claude/tests/fixtures/expected_toc_anchors.txt
vendored
Normal file
40
.agent/knowledge/awesome_claude/tests/fixtures/expected_toc_anchors.txt
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
agent-skills-
|
||||
alternative-clients-
|
||||
awesome-claude-code
|
||||
ci--deployment
|
||||
claudemd-files-
|
||||
code-analysis--testing
|
||||
config-managers
|
||||
contents
|
||||
context-loading--priming
|
||||
contributing-
|
||||
documentation--changelogs
|
||||
domain-specific
|
||||
general
|
||||
general-1
|
||||
general-2
|
||||
general-3
|
||||
general-4
|
||||
general-5
|
||||
general-6
|
||||
general-7
|
||||
growing-thanks-to-you
|
||||
hooks-
|
||||
ide-integrations
|
||||
language-specific
|
||||
latest-additions
|
||||
license
|
||||
miscellaneous
|
||||
official-documentation-️
|
||||
orchestrators
|
||||
pick-your-style
|
||||
project--task-management
|
||||
project-scaffolding--mcp
|
||||
ralph-wiggum
|
||||
recommend-a-new-resource-here
|
||||
slash-commands-
|
||||
status-lines-
|
||||
tooling-
|
||||
usage-monitors
|
||||
version-control--git
|
||||
workflows--knowledge-guides-
|
||||
371
.agent/knowledge/awesome_claude/tests/fixtures/github-html/awesome-root.html
vendored
Normal file
371
.agent/knowledge/awesome_claude/tests/fixtures/github-html/awesome-root.html
vendored
Normal file
@@ -0,0 +1,371 @@
|
||||
<article class="markdown-body entry-content container-lg" itemprop="text">
|
||||
<div class="markdown-heading" dir="auto"><h3 align="center" tabindex="-1" class="heading-element" dir="auto">Pick Your Style:</h3><a id="user-content-pick-your-style" class="anchor" aria-label="Permalink: Pick Your Style:" href="#pick-your-style"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<p align="center" dir="auto">
|
||||
<a href="/hesreallyhim/awesome-claude-code/blob/main"><img src="/hesreallyhim/awesome-claude-code/raw/main/assets/badge-style-awesome.svg" alt="Awesome" height="28" style="max-width: 100%; height: auto; max-height: 28px;"></a>
|
||||
<a href="/hesreallyhim/awesome-claude-code/blob/main/README_ALTERNATIVES/README_EXTRA.md"><img src="/hesreallyhim/awesome-claude-code/raw/main/assets/badge-style-extra.svg" alt="Extra" height="28" style="max-width: 100%; height: auto; max-height: 28px;"></a>
|
||||
<a href="/hesreallyhim/awesome-claude-code/blob/main/README_ALTERNATIVES/README_CLASSIC.md"><img src="/hesreallyhim/awesome-claude-code/raw/main/assets/badge-style-classic.svg" alt="Classic" height="28" style="max-width: 100%; height: auto; max-height: 28px;"></a>
|
||||
<a href="/hesreallyhim/awesome-claude-code/blob/main/README_ALTERNATIVES/README_FLAT_ALL_AZ.md"><img src="/hesreallyhim/awesome-claude-code/raw/main/assets/badge-style-flat.svg" alt="Flat" height="28" style="max-width: 100%; height: auto; max-height: 28px;"></a>
|
||||
</p>
|
||||
<div class="markdown-heading" dir="auto"><h1 tabindex="-1" class="heading-element" dir="auto">Awesome Claude Code</h1><a id="user-content-awesome-claude-code" class="anchor" aria-label="Permalink: Awesome Claude Code" href="#awesome-claude-code"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<p dir="auto"><a href="https://awesome.re" rel="nofollow"><img src="https://camo.githubusercontent.com/9d49598b873146ec650fb3f275e8a532c765dabb1f61d5afa25be41e79891aa7/68747470733a2f2f617765736f6d652e72652f62616467652e737667" alt="Awesome" data-canonical-src="https://awesome.re/badge.svg" style="max-width: 100%;"></a></p>
|
||||
<blockquote>
|
||||
<p dir="auto">A curated list of slash-commands, CLAUDE.md files, CLI tools, and other resources for enhancing your <a href="https://docs.anthropic.com/en/docs/claude-code" rel="nofollow">Claude Code</a> workflow.</p>
|
||||
</blockquote>
|
||||
<p dir="auto">Claude Code is a CLI-based coding assistant from <a href="https://www.anthropic.com/" rel="nofollow">Anthropic</a> that you can access in your terminal or IDE. This list helps the community share knowledge and best practices.</p>
|
||||
<div align="center" dir="auto">
|
||||
<a target="_blank" rel="noopener noreferrer" href="/hesreallyhim/awesome-claude-code/blob/main/assets/repo-ticker-awesome.svg"><img src="/hesreallyhim/awesome-claude-code/raw/main/assets/repo-ticker-awesome.svg" alt="Featured Claude Code Projects" width="100%" style="max-width: 100%;"></a>
|
||||
</div>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Latest Additions</h2><a id="user-content-latest-additions" class="anchor" aria-label="Permalink: Latest Additions" href="#latest-additions"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/danielrosehill/Claude-Code-Repos-Index">Claude Code Repos Index</a> by <a href="https://github.com/danielrosehill">Daniel Rosehill</a> - This is either the work of a prolific genius, or a very clever bot (or both), although it hardly matters because the quality is so good - an index of 75+ Claude Code repositories published by the author - and I'm not talking about slop. CMS, system design, deep research, IoT, agentic workflows, server management, personal health... If you spot the lie, let me know, otherwise please check these out.</li>
|
||||
<li><a href="https://github.com/ykdojo/claude-code-tips">Claude Code Tips</a> by <a href="https://github.com/ykdojo">ykdojo</a> - A nice variety of 35+ brief but information-dense Claude Code tips covering voice input, system prompt patching, container workflows for risky tasks, conversation cloning(!), multi-model orchestration with Gemini CLI, and plenty more. Nice demos, working scripts, a plugin, I'd say this probably has a little something for everyone.</li>
|
||||
<li><a href="https://github.com/obra/superpowers">Superpowers</a> by <a href="https://github.com/obra">Jesse Vincent</a> - A strong bundle of core competencies for software engineering, with good coverage of a large portion of the SDLC - from planning, reviewing, testing, debugging... Well written, well organized, and adaptable. The author refers to them as "superpowers", but many of them are just consolidating engineering best practices - which sometimes does feel like a superpower when working with Claude Code.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Contents</h2><a id="user-content-contents" class="anchor" aria-label="Permalink: Contents" href="#contents"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="#agent-skills--">Agent Skills 🤖</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general-">General</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#workflows--knowledge-guides--">Workflows & Knowledge Guides 🧠</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--1">General</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#tooling--">Tooling 🧰</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--2">General</a></li>
|
||||
<li><a href="#ide-integrations-">IDE Integrations</a></li>
|
||||
<li><a href="#usage-monitors-">Usage Monitors</a></li>
|
||||
<li><a href="#orchestrators-">Orchestrators</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#status-lines--">Status Lines 📊</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--3">General</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#hooks--">Hooks 🪝</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--4">General</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#slash-commands--">Slash-Commands 🔪</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--5">General</a></li>
|
||||
<li><a href="#version-control--git-">Version Control & Git</a></li>
|
||||
<li><a href="#code-analysis--testing-">Code Analysis & Testing</a></li>
|
||||
<li><a href="#context-loading--priming-">Context Loading & Priming</a></li>
|
||||
<li><a href="#documentation--changelogs-">Documentation & Changelogs</a></li>
|
||||
<li><a href="#ci--deployment-">CI / Deployment</a></li>
|
||||
<li><a href="#project--task-management-">Project & Task Management</a></li>
|
||||
<li><a href="#miscellaneous-">Miscellaneous</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#claudemd-files--">CLAUDE.md Files 📂</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#language-specific-">Language-Specific</a></li>
|
||||
<li><a href="#domain-specific-">Domain-Specific</a></li>
|
||||
<li><a href="#project-scaffolding--mcp-">Project Scaffolding & MCP</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#alternative-clients--">Alternative Clients 📱</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--6">General</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#official-documentation--%EF%B8%8F">Official Documentation 🏛️</a>
|
||||
<ul dir="auto">
|
||||
<li><a href="#general--7">General</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Agent Skills 🤖</h2><a id="user-content-agent-skills-" class="anchor" aria-label="Permalink: Agent Skills 🤖" href="#agent-skills-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">Agent skills are model-controlled configurations (files, scripts, resources, etc.) that enable Claude Code to perform specialized tasks requiring specific knowledge or capabilities.</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general" class="anchor" aria-label="Permalink: General" href="#general"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/fcakyon/claude-codex-settings">Claude Codex Settings</a> by <a href="https://github.com/fcakyon">fatih akyon</a> - A well-organized, well-written set of plugins covering core developer activities, such as working with common cloud platforms like GitHub, Azure, MongoDB, and popular services such as Tavily, Playwright, and more. Clear, not overly-opinionated, and compatible with a few other providers.</li>
|
||||
<li><a href="https://github.com/dreamiurg/claude-mountaineering-skills">Claude Mountaineering Skills</a> by <a href="https://github.com/dreamiurg">Dmytro Gaivoronsky</a> - Claude Code skill that automates mountain route research for North American peaks. Aggregates data from 10+ mountaineering sources like Mountaineers.org, PeakBagger.com and SummitPost.com to generate detailed route beta reports with weather, avalanche conditions, and trip reports.</li>
|
||||
<li><a href="https://github.com/skills-directory/skill-codex">Codex Skill</a> by <a href="https://github.com/klaudworks">klaudworks</a> - Enables users to prompt codex from claude code. Unlike the raw codex mcp server, this skill infers parameters such as model, reasoning effort, sandboxing from your prompt or asks you to specify them. It also simplifies continuing prior codex sessions so that codex can continue with the prior context.</li>
|
||||
<li><a href="https://github.com/NeoLabHQ/context-engineering-kit">Context Engineering Kit</a> by <a href="https://github.com/LeoVS09">Vlad Goncharov</a> - Hand-crafted collection of advanced context engineering techniques and patterns with minimal token footprint focused on improving agent result quality.</li>
|
||||
<li><a href="https://github.com/obra/superpowers">Superpowers</a> by <a href="https://github.com/obra">Jesse Vincent</a> - A strong bundle of core competencies for software engineering, with good coverage of a large portion of the SDLC - from planning, reviewing, testing, debugging... Well written, well organized, and adaptable. The author refers to them as "superpowers", but many of them are just consolidating engineering best practices - which sometimes does feel like a superpower when working with Claude Code.</li>
|
||||
<li><a href="https://github.com/glittercowboy/taches-cc-resources">TÂCHES Claude Code Resources</a> by <a href="https://github.com/glittercowboy">TÂCHES</a> - A well-balanced, "down-to-Earth" set of sub agents, skills, and commands, that are well-organized, easy to read, and a healthy focus on "meta"-skills/agents, like "skill-auditor", hook creation, etc. - the kind of things you can adapt to your workflow, and not the other way around.</li>
|
||||
<li><a href="https://github.com/alonw0/web-asset-generator">Web Assets Generator Skill</a> by <a href="https://github.com/alonw0">Alon Wolenitz</a> - Easily generate web assets from Claude Code including favicons, app icons (PWA), and social media meta images (Open Graph) for Facebook, Twitter, WhatsApp, and LinkedIn. Handles image resizing, text-to-image generation, emojis, and provides proper HTML meta tags.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Workflows & Knowledge Guides 🧠</h2><a id="user-content-workflows--knowledge-guides-" class="anchor" aria-label="Permalink: Workflows & Knowledge Guides 🧠" href="#workflows--knowledge-guides-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">A workflow is a tightly coupled set of Claude Code-native resources that facilitate specific projects</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-1" class="anchor" aria-label="Permalink: General" href="#general-1"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/ayoubben18/ab-method">AB Method</a> by <a href="https://github.com/ayoubben18">Ayoub Bensalah</a> - A principled, spec-driven workflow that transforms large problems into focused, incremental missions using Claude Code's specialized sub agents. Includes slash-commands, sub agents, and specialized workflows designed for specific parts of the SDLC.</li>
|
||||
<li><a href="https://github.com/ThibautMelen/agentic-workflow-patterns">Agentic Workflow Patterns</a> by <a href="https://github.com/ThibautMelen">ThibautMelen</a> - A comprehensive and well-documented collection of agentic patterns from Anthropic docs, with colorful Mermaid diagrams and code examples for each pattern. Covers Subagent Orchestration, Progressive Skills, Parallel Tool Calling, Master-Clone Architecture, Wizard Workflows, and more. Also compatible with other providers.</li>
|
||||
<li><a href="https://github.com/cloudartisan/cloudartisan.github.io/tree/main/.claude/commands">Blogging Platform Instructions</a> by <a href="https://github.com/cloudartisan">cloudartisan</a> - Provides a well-structured set of commands for publishing and maintaining a blogging platform, including commands for creating posts, managing categories, and handling media files.</li>
|
||||
<li><a href="https://github.com/ericbuess/claude-code-docs">Claude Code Documentation Mirror</a> by <a href="https://github.com/ericbuess">Eric Buess</a> - A mirror of the Anthropic © PBC documentation pages for Claude Code, updated every few hours. Can come in handy when trying to stay on top of the ever-expanding feature-set of Dr. Claw D. Code, Ph.D.</li>
|
||||
<li><a href="https://nikiforovall.blog/claude-code-rules/" rel="nofollow">Claude Code Handbook</a> by <a href="https://github.com/nikiforovall">nikiforovall</a> - Collection of best practices, tips, and techniques for Claude Code development workflows, enhanced with distributable plugins.</li>
|
||||
<li><a href="https://github.com/diet103/claude-code-infrastructure-showcase">Claude Code Infrastructure Showcase</a> by <a href="https://github.com/diet103">diet103</a> - A remarkably innovative approach to working with Skills, the centerpiece of which being a technique that leverages hooks to ensure that Claude intelligently selects and activates the appropriate Skill given the current context. Well-documented and adaptable to different projects and workflows.</li>
|
||||
<li><a href="https://github.com/automazeio/ccpm">Claude Code PM</a> by <a href="https://github.com/ranaroussi">Ran Aroussi</a> - Really comprehensive and feature-packed project-management workflow for Claude Code. Numerous specialized agents, slash-commands, and strong documentation.</li>
|
||||
<li><a href="https://github.com/danielrosehill/Claude-Code-Repos-Index">Claude Code Repos Index</a> by <a href="https://github.com/danielrosehill">Daniel Rosehill</a> - This is either the work of a prolific genius, or a very clever bot (or both), although it hardly matters because the quality is so good - an index of 75+ Claude Code repositories published by the author - and I'm not talking about slop. CMS, system design, deep research, IoT, agentic workflows, server management, personal health... If you spot the lie, let me know, otherwise please check these out.</li>
|
||||
<li><a href="https://github.com/Piebald-AI/claude-code-system-prompts">Claude Code System Prompts</a> by <a href="https://github.com/Piebald-AI">Piebald AI</a> - All parts of Claude Code's system prompt, including builtin tool descriptions, sub agent prompts (Plan/Explore/Task), utility prompts (CLAUDE.md, compact, Bash cmd, security review, agent creation, etc.). Updated for each Claude Code version.</li>
|
||||
<li><a href="https://github.com/ykdojo/claude-code-tips">Claude Code Tips</a> by <a href="https://github.com/ykdojo">ykdojo</a> - A nice variety of 35+ brief but information-dense Claude Code tips covering voice input, system prompt patching, container workflows for risky tasks, conversation cloning(!), multi-model orchestration with Gemini CLI, and plenty more. Nice demos, working scripts, a plugin, I'd say this probably has a little something for everyone.</li>
|
||||
<li><a href="https://github.com/maxritter/claude-codepro">Claude CodePro</a> by <a href="https://www.maxritter.net" rel="nofollow">Max Ritter</a> - Professional development environment for Claude Code with spec-driven workflow, TDD enforcement, cross-session memory, semantic search, quality hooks, and modular rules integration. A bit "heavyweight" but feature-packed and has wide coverage.</li>
|
||||
<li><a href="https://github.com/costiash/claude-code-docs">claude-code-docs</a> by <a href="https://github.com/costiash">Constantin Shafranski</a> - A mirror of the Anthropic© PBC documentation site for Claude/Code, but with bonus features like full-text search and query-time updates - a nice companion to <code>claude-code-docs</code> for up-to-the-minute, fully-indexed information so that Claude Code can read about itself.</li>
|
||||
<li><a href="https://github.com/JSONbored/claudepro-directory">ClaudoPro Directory</a> by <a href="https://github.com/JSONbored">ghost</a> - Well-crafted, wide selection of Claude Code hooks, slash commands, subagent files, and more, covering a range of specialized tasks and workflows. Better resources than your average "Claude-template-for-everything" site.</li>
|
||||
<li><a href="https://github.com/disler/just-prompt/tree/main/.claude/commands">Context Priming</a> by <a href="https://github.com/disler">disler</a> - Provides a systematic approach to priming Claude Code with comprehensive project context through specialized commands for different project scenarios and development contexts.</li>
|
||||
<li><a href="https://github.com/OneRedOak/claude-code-workflows/tree/main/design-review">Design Review Workflow</a> by <a href="https://github.com/OneRedOak">Patrick Ellis</a> - A tailored workflow for enabling automated UI/UX design review, including specialized sub agents, slash commands, <code>CLAUDE.md</code> excerpts, and more. Covers a broad range of criteria from responsive design to accessibility.</li>
|
||||
<li><a href="https://github.com/tott/laravel-tall-claude-ai-configs">Laravel TALL Stack AI Development Starter Kit</a> by <a href="https://github.com/tott">tott</a> - Transform your Laravel TALL (Tailwind, AlpineJS, Laravel, Livewire) stack development with comprehensive Claude Code configurations that provide intelligent assistance, systematic workflows, and domain expert consultation.</li>
|
||||
<li><a href="https://github.com/cheukyin175/learn-faster-kit">learn-faster-kit</a> by <a href="https://github.com/cheukyin175">Hugo Lau</a> - A creative educational framework for Claude Code, inspired by the "FASTER" approach to self-teaching. Ships with a variety of agents, slash commands, and tools that enable Claude Code to help you progress at your own pace, employing well-established pedagogical techniques like active learning and spaced repetition.</li>
|
||||
<li><a href="https://github.com/kingler/n8n_agent/tree/main/.claude/commands">n8n_agent</a> by <a href="https://github.com/kingler">kingler</a> - Amazing comprehensive set of comments for code analysis, QA, design, documentation, project structure, project management, optimization, and many more.</li>
|
||||
<li><a href="https://github.com/steadycursor/steadystart/tree/main/.claude/commands">Project Bootstrapping and Task Management</a> by <a href="https://github.com/steadycursor">steadycursor</a> - Provides a structured set of commands for bootstrapping and managing a new project, including meta-commands for creating and editing custom slash-commands.</li>
|
||||
<li><a href="https://github.com/scopecraft/command/tree/main/.claude/commands">Project Management, Implementation, Planning, and Release</a> by <a href="https://github.com/scopecraft">scopecraft</a> - Really comprehensive set of commands for all aspects of SDLC.</li>
|
||||
<li><a href="https://github.com/harperreed/dotfiles/tree/master/.claude/commands">Project Workflow System</a> by <a href="https://github.com/harperreed">harperreed</a> - A set of commands that provide a comprehensive workflow system for managing projects, including task management, code review, and deployment processes.</li>
|
||||
<li><a href="https://github.com/tony/claude-code-riper-5">RIPER Workflow</a> by <a href="https://tony.sh" rel="nofollow">Tony Narlock</a> - Structured development workflow enforcing separation between Research, Innovate, Plan, Execute, and Review phases. Features consolidated subagents for context-efficiency, branch-aware memory bank, and strict mode enforcement for guided development.</li>
|
||||
<li><a href="https://diwank.space/field-notes-from-shipping-real-code-with-claude" rel="nofollow">Shipping Real Code w/ Claude</a> by <a href="https://github.com/creatorrr">Diwank</a> - A detailed blog post explaining the author's process for shipping a product with Claude Code, including CLAUDE.md files and other interesting resources.</li>
|
||||
<li><a href="https://github.com/Helmi/claude-simone">Simone</a> by <a href="https://github.com/Helmi">Helmi</a> - A broader project management workflow for Claude Code that encompasses not just a set of commands, but a system of documents, guidelines, and processes to facilitate project planning and execution.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Ralph Wiggum</h3><a id="user-content-ralph-wiggum" class="anchor" aria-label="Permalink: Ralph Wiggum" href="#ralph-wiggum"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/frankbria/ralph-claude-code">Ralph for Claude Code</a> by <a href="https://github.com/frankbria">Frank Bria</a> - An autonomous AI development framework that enables Claude Code to work iteratively on projects until completion.</li>
|
||||
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum">Ralph Wiggum Plugin</a> by <a href="https://github.com/anthropics">Anthropic PBC</a> - The official Anthropic implementation of the Ralph Wiggum technique for iterative AI development loops.</li>
|
||||
<li><a href="https://github.com/mikeyobrien/ralph-orchestrator">ralph-orchestrator</a> by <a href="https://github.com/mikeyobrien">mikeyobrien</a> - Ralph Orchestrator implements the Ralph Wiggum technique for autonomous task completion.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Tooling 🧰</h2><a id="user-content-tooling-" class="anchor" aria-label="Permalink: Tooling 🧰" href="#tooling-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">Tooling denotes applications that are built on top of Claude Code and consist of more components than slash-commands and <code>CLAUDE.md</code> files</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-2" class="anchor" aria-label="Permalink: General" href="#general-2"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/GWUDCAP/cc-sessions">cc-sessions</a> by <a href="https://github.com/satoastshi">toastdev</a> - An opinionated approach to productive development with Claude Code.</li>
|
||||
<li><a href="https://github.com/Veraticus/cc-tools">cc-tools</a> by <a href="https://github.com/Veraticus">Josh Symonds</a> - High-performance Go implementation of Claude Code hooks and utilities. Provides smart linting, testing, and statusline generation with minimal overhead.</li>
|
||||
<li><a href="https://github.com/nyatinte/ccexp">ccexp</a> by <a href="https://github.com/nyatinte">nyatinte</a> - Interactive CLI tool for discovering and managing Claude Code configuration files and slash commands with a beautiful terminal UI.</li>
|
||||
<li><a href="https://github.com/eckardt/cchistory">cchistory</a> by <a href="https://github.com/eckardt">eckardt</a> - Like the shell history command but for your Claude Code sessions. Easily list all Bash or "Bash-mode" (<code>!</code>) commands Claude Code ran in a session for reference.</li>
|
||||
<li><a href="https://github.com/Brads3290/cclogviewer">cclogviewer</a> by <a href="https://github.com/Brads3290">Brad S.</a> - A humble but handy utility for viewing Claude Code <code>.jsonl</code> conversation files in a pretty HTML UI.</li>
|
||||
<li><a href="https://github.com/davila7/claude-code-templates">Claude Code Templates</a> by <a href="https://github.com/davila7">Daniel Avila</a> - Incredibly awesome collection of resources from every category in this list, presented with a neatly polished UI, great features like usage dashboard, analytics, and everything from slash commands to hooks to agents. An awesome companion for this awesome list.</li>
|
||||
<li><a href="https://github.com/possibilities/claude-composer">Claude Composer</a> by <a href="https://github.com/possibilities">Mike Bannister</a> - A tool that adds small enhancements to Claude Code.</li>
|
||||
<li><a href="https://github.com/claude-did-this/claude-hub">Claude Hub</a> by <a href="https://github.com/claude-did-this">Claude Did This</a> - A webhook service that connects Claude Code to GitHub repositories, enabling AI-powered code assistance directly through pull requests and issues. This integration allows Claude to analyze repositories, answer technical questions, and help developers understand and improve their codebase through simple @mentions.</li>
|
||||
<li><a href="https://github.com/pchalasani/claude-code-tools">claude-code-tools</a> by <a href="https://github.com/pchalasani">Prasad Chalasani</a> - Well-crafted toolset for session continuity, featuring skills/commands to avoid compaction and recover context across sessions with cross-agent handoff between Claude Code and Codex CLI. Includes a fast Rust/Tantivy-powered full-text session search (TUI for humans, skill/CLI for agents), tmux-cli skill + command for interacting with scripts and CLI agents, and safety hooks to block dangerous commands.</li>
|
||||
<li><a href="https://github.com/serpro69/claude-starter-kit">claude-starter-kit</a> by <a href="https://github.com/serpro69">serpro69</a> - This is a starter template repository designed to provide a complete development environment for Claude-Code with pre-configured MCP servers and tools for AI-powered development workflows. The repository is intentionally minimal, containing only configuration templates for three primary systems: Claude Code, Serena, and Task Master.</li>
|
||||
<li><a href="https://github.com/carlrannaberg/claudekit">claudekit</a> by <a href="https://github.com/carlrannaberg">Carl Rannaberg</a> - Impressive CLI toolkit providing auto-save checkpointing, code quality hooks, specification generation and execution, and 20+ specialized subagents including oracle (gpt-5), code-reviewer (6-aspect deep analysis), ai-sdk-expert (Vercel AI SDK), typescript-expert and many more for Claude Code workflows.</li>
|
||||
<li><a href="https://github.com/dagger/container-use">Container Use</a> by <a href="https://github.com/dagger">dagger</a> - Development environments for coding agents. Enable multiple agents to work safely and independently with your preferred stack.</li>
|
||||
<li><a href="https://github.com/FlineDev/ContextKit">ContextKit</a> by <a href="https://github.com/Jeehut">Cihat Gündüz</a> - A systematic development framework that transforms Claude Code into a proactive development partner. Features 4-phase planning methodology, specialized quality agents, and structured workflows that help AI produce production-ready code on first try.</li>
|
||||
<li><a href="https://github.com/zippoxer/recall">recall</a> by <a href="https://github.com/zippoxer">zippoxer</a> - Full-text search your Claude Code sessions. Run <code>recall</code> in terminal, type to search, Enter to resume. Alternative to <code>claude --resume</code>.</li>
|
||||
<li><a href="https://github.com/dyoshikawa/rulesync">Rulesync</a> by <a href="https://github.com/dyoshikawa">dyoshikawa</a> - A Node.js CLI tool that automatically generates configs (rules, ignore files, MCP servers, commands, and subagents) for various AI coding agents. Rulesync can convert configs between Claude Code and other AI agents in both directions.</li>
|
||||
<li><a href="https://github.com/icanhasjonas/run-claude-docker">run-claude-docker</a> by <a href="https://github.com/icanhasjonas/">Jonas</a> - A self-contained Docker runner that forwards your current workspace into a safe(r) isolated docker container, where you still have access to your Claude Code settings, authentication, ssh agent, pgp, optionally aws keys etc.</li>
|
||||
<li><a href="https://github.com/marcindulak/stt-mcp-server-linux">stt-mcp-server-linux</a> by <a href="https://github.com/marcindulak">marcindulak</a> - A push-to-talk speech transcription setup for Linux using a Python MCP server. Runs locally in Docker with no external API calls. Your speech is recorded, transcribed into text, and then sent to Claude running in a Tmux session.</li>
|
||||
<li><a href="https://github.com/SuperClaude-Org/SuperClaude_Framework">SuperClaude</a> by <a href="https://github.com/SuperClaude-Org">SuperClaude-Org</a> - A versatile configuration framework that enhances Claude Code with specialized commands, cognitive personas, and development methodologies, such as "Introspection" and "Orchestration".</li>
|
||||
<li><a href="https://github.com/Piebald-AI/tweakcc">tweakcc</a> by <a href="https://github.com/Piebald-AI">Piebald-AI</a> - Command-line tool to customize your Claude Code styling.</li>
|
||||
<li><a href="https://github.com/vibe-log/vibe-log-cli">Vibe-Log</a> by <a href="https://github.com/vibe-log">Vibe-Log</a> - Analyzes your Claude Code prompts locally (using CC), provides intelligent session analysis and actionable strategic guidance - works in the statusline and produces very pretty HTML reports as well. Easy to install and remove.</li>
|
||||
<li><a href="https://github.com/OverseedAI/viwo">viwo-cli</a> by <a href="https://github.com/hal-shin">Hal Shin</a> - Run Claude Code in a Docker container with git worktrees as volume mounts to enable safer usage of <code>--dangerously-skip-permissions</code> for frictionless one-shotting prompts. Allows users to spin up multiple instances of Claude Code in the background easily with reduced permission fatigue.</li>
|
||||
<li><a href="https://github.com/mbailey/voicemode">VoiceMode MCP</a> by <a href="https://github.com/mbailey">Mike Bailey</a> - VoiceMode MCP brings natural conversations to Claude Code. It supports any OpenAI API compatible voice services and installs free and open source voice services (Whisper.cpp and Kokoro-FastAPI).</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">IDE Integrations</h3><a id="user-content-ide-integrations" class="anchor" aria-label="Permalink: IDE Integrations" href="#ide-integrations"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://marketplace.visualstudio.com/items?itemName=AndrePimenta.claude-code-chat" rel="nofollow">Claude Code Chat</a> by <a href="https://github.com/andrepimenta">andrepimenta</a> - An elegant and user-friendly Claude Code chat interface for VS Code.</li>
|
||||
<li><a href="https://github.com/manzaltu/claude-code-ide.el">claude-code-ide.el</a> by <a href="https://github.com/manzaltu">manzaltu</a> - claude-code-ide.el integrates Claude Code with Emacs, like Anthropic’s VS Code/IntelliJ extensions. It shows ediff-based code suggestions, pulls LSP/flymake/flycheck diagnostics, and tracks buffer context. It adds an extensible MCP tool support for symbol refs/defs, project metadata, and tree-sitter AST queries.</li>
|
||||
<li><a href="https://github.com/stevemolitor/claude-code.el">claude-code.el</a> by <a href="https://github.com/stevemolitor">stevemolitor</a> - An Emacs interface for Claude Code CLI.</li>
|
||||
<li><a href="https://github.com/greggh/claude-code.nvim">claude-code.nvim</a> by <a href="https://github.com/greggh">greggh</a> - A seamless integration between Claude Code AI assistant and Neovim.</li>
|
||||
<li><a href="https://github.com/Haleclipse/Claudix">Claudix - Claude Code for VSCode</a> by <a href="https://github.com/Haleclipse">Haleclipse</a> - A VSCode extension that brings Claude Code directly into your editor with interactive chat interface, session management, intelligent file operations, terminal execution, and real-time streaming responses. Built with Vue 3, TypeScript.</li>
|
||||
<li><a href="https://github.com/stravu/crystal">crystal</a> by <a href="https://github.com/stravu">stravu</a> - A full-fledged desktop application for orchestrating, monitoring, and interacting with Claude Code agents.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Usage Monitors</h3><a id="user-content-usage-monitors" class="anchor" aria-label="Permalink: Usage Monitors" href="#usage-monitors"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/ryoppippi/ccusage">CC Usage</a> by <a href="https://github.com/ryoppippi">ryoppippi</a> - Handy CLI tool for managing and analyzing Claude Code usage, based on analyzing local Claude Code logs. Presents a nice dashboard regarding cost information, token consumption, etc.</li>
|
||||
<li><a href="https://github.com/snipeship/ccflare">ccflare</a> by <a href="https://github.com/snipeship">snipeship</a> - Claude Code usage dashboard with a web-UI that would put Tableau to shame. Thoroughly comprehensive metrics, frictionless setup, detailed logging, really really nice UI.</li>
|
||||
<li><a href="https://github.com/tombii/better-ccflare/">ccflare -> <strong>better-ccflare</strong></a> by <a href="https://github.com/tombii">tombii</a> - A well-maintained and feature-enhanced fork of the glorious <code>ccflare</code> usage dashboard by @snipeship (which at the time of writing has not had an update in a few months). <code>better-ccflare</code> builds on this foundation with some performance enhancements, extended provider support, bug fixes, Docker deployment, and more.</li>
|
||||
<li><a href="https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor">Claude Code Usage Monitor</a> by <a href="https://github.com/Maciek-roboblog">Maciek-roboblog</a> - A real-time terminal-based tool for monitoring Claude Code token usage. It shows live token consumption, burn rate, and predictions for token depletion. Features include visual progress bars, session-aware analytics, and support for multiple subscription plans.</li>
|
||||
<li><a href="https://github.com/kunwar-shah/claudex">Claudex</a> by <a href="https://github.com/kunwar-shah">Kunwar Shah</a> - Claudex - A web-based browser for exploring your Claude Code conversation history across projects. Indexes your codebase for full-text search. Nice, easy-to-navigate UI. Simple dashboard interface for high-level analytics, and multiple export options as well. (And completely local w/ no telemetry!).</li>
|
||||
<li><a href="https://github.com/sculptdotfun/viberank">viberank</a> by <a href="https://github.com/nikshepsvn">nikshepsvn</a> - A community-driven leaderboard tool that enables developers to visualize, track, and compete based on their Claude Code usage statistics. It features robust data analytics, GitHub OAuth, data validation, and user-friendly CLI/web submission methods.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Orchestrators</h3><a id="user-content-orchestrators" class="anchor" aria-label="Permalink: Orchestrators" href="#orchestrators"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/ruvnet/claude-code-flow">Claude Code Flow</a> by <a href="https://github.com/ruvnet">ruvnet</a> - This mode serves as a code-first orchestration layer, enabling Claude to write, edit, test, and optimize code autonomously across recursive agent cycles.</li>
|
||||
<li><a href="https://github.com/smtg-ai/claude-squad">Claude Squad</a> by <a href="https://github.com/smtg-ai">smtg-ai</a> - Claude Squad is a terminal app that manages multiple Claude Code, Codex (and other local agents including Aider) in separate workspaces, allowing you to work on multiple tasks simultaneously.</li>
|
||||
<li><a href="https://github.com/parruda/claude-swarm">Claude Swarm</a> by <a href="https://github.com/parruda">parruda</a> - Launch Claude Code session that is connected to a swarm of Claude Code Agents.</li>
|
||||
<li><a href="https://github.com/eyaltoledano/claude-task-master">Claude Task Master</a> by <a href="https://github.com/eyaltoledano">eyaltoledano</a> - A task management system for AI-driven development with Claude, designed to work seamlessly with Cursor AI.</li>
|
||||
<li><a href="https://github.com/grahama1970/claude-task-runner">Claude Task Runner</a> by <a href="https://github.com/grahama1970">grahama1970</a> - A specialized tool to manage context isolation and focused task execution with Claude Code, solving the critical challenge of context length limitations and task focus when working with Claude on complex, multi-step projects.</li>
|
||||
<li><a href="https://github.com/slopus/happy">Happy Coder</a> by <a href="https://peoplesgrocers.com/en/projects" rel="nofollow">GrocerPublishAgent</a> - Spawn and control multiple Claude Codes in parallel from your phone or desktop. Happy Coder runs Claude Code on your hardware, sends push notifications when Claude needs more input or permission, and costs nothing.</li>
|
||||
<li><a href="https://github.com/rsmdt/the-startup">The Agentic Startup</a> by <a href="https://github.com/rsmdt">Rudolf Schmidt</a> - Yet Another Claude Orchestrator - a collection of agents, commands, etc., for shipping production code - but I like this because it's comprehensive, well-written, and one of the few resources that actually uses Output Styles! +10 points!</li>
|
||||
<li><a href="https://github.com/dtormoen/tsk">TSK - AI Agent Task Manager and Sandbox</a> by <a href="https://github.com/dtormoen">dtormoen</a> - A Rust CLI tool that lets you delegate development tasks to AI agents running in sandboxed Docker environments. Multiple agents work in parallel, returning git branches for human review.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Config Managers</h3><a id="user-content-config-managers" class="anchor" aria-label="Permalink: Config Managers" href="#config-managers"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/foxj77/claudectx">ClaudeCTX</a> by <a href="https://github.com/foxj77">John Fox</a> - claudectx lets you switch your entire Claude Code configuration with a single command.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Status Lines 📊</h2><a id="user-content-status-lines-" class="anchor" aria-label="Permalink: Status Lines 📊" href="#status-lines-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">Status lines - Configurations and customizations for Claude Code's status bar functionality</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-3" class="anchor" aria-label="Permalink: General" href="#general-3"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/Haleclipse/CCometixLine">CCometixLine - Claude Code Statusline</a> by <a href="https://github.com/Haleclipse">Haleclipse</a> - A high-performance Claude Code statusline tool written in Rust with Git integration, usage tracking, interactive TUI configuration, and Claude Code enhancement utilities.</li>
|
||||
<li><a href="https://github.com/sirmalloc/ccstatusline">ccstatusline</a> by <a href="https://github.com/sirmalloc">sirmalloc</a> - A highly customizable status line formatter for Claude Code CLI that displays model info, git branch, token usage, and other metrics in your terminal.</li>
|
||||
<li><a href="https://github.com/rz1989s/claude-code-statusline">claude-code-statusline</a> by <a href="https://github.com/rz1989s">rz1989s</a> - Enhanced 4-line statusline for Claude Code with themes, cost tracking, and MCP server monitoring.</li>
|
||||
<li><a href="https://github.com/Owloops/claude-powerline">claude-powerline</a> by <a href="https://github.com/Owloops">Owloops</a> - A vim-style powerline statusline for Claude Code with real-time usage tracking, git integration, custom themes, and more.</li>
|
||||
<li><a href="https://github.com/hagan/claudia-statusline">claudia-statusline</a> by <a href="https://github.com/hagan">Hagan Franks</a> - High-performance Rust-based statusline for Claude Code with persistent stats tracking, progress bars, and optional cloud sync. Features SQLite-first persistence, git integration, context progress bars, burn rate calculation, XDG-compliant with theme support (dark/light, NO_COLOR).</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Hooks 🪝</h2><a id="user-content-hooks-" class="anchor" aria-label="Permalink: Hooks 🪝" href="#hooks-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">Hooks are a powerful API for Claude Code that allows users to activate commands and run scripts at different points in Claude's agentic lifecycle.</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-4" class="anchor" aria-label="Permalink: General" href="#general-4"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/Talieisin/britfix">Britfix</a> by <a href="https://github.com/Talieisin">Talieisin</a> - Claude outputs American spellings by default, which can have an impact on: professional credibility, compliance, documentation, and more. Britfix converts to British English, with a Claude Code hook for automatic conversion as files are written. Context-aware: handles code files intelligently by only converting comments and docstrings, never identifiers or string literals.</li>
|
||||
<li><a href="https://github.com/dazuiba/CCNotify">CC Notify</a> by <a href="https://github.com/dazuiba">dazuiba</a> - CCNotify provides desktop notifications for Claude Code, alerting you to input needs or task completion, with one-click jumps back to VS Code and task duration display.</li>
|
||||
<li><a href="https://github.com/GowayLee/cchooks">cchooks</a> by <a href="https://github.com/GowayLee">GowayLee</a> - A lightweight Python SDK with a clean API and good documentation; simplifies the process of writing hooks and integrating them into your codebase, providing a nice abstraction over the JSON configuration files.</li>
|
||||
<li><a href="https://github.com/aannoo/claude-hook-comms">Claude Code Hook Comms (HCOM)</a> by <a href="https://github.com/aannoo">aannoo</a> - Lightweight CLI tool for real-time communication between Claude Code sub agents using hooks. Enables multi-agent collaboration with @-mention targeting, live dashboard monitoring, and zero-dependency implementation. [NOTE: At the time of posting, this resource is a little unstable - I'm sharing it anyway, because I think it's incredibly promising and creative. I hope by the time you read this, it is production-ready.].</li>
|
||||
<li><a href="https://github.com/beyondcode/claude-hooks-sdk">claude-code-hooks-sdk</a> by <a href="https://github.com/beyondcode">beyondcode</a> - A Laravel-inspired PHP SDK for building Claude Code hook responses with a clean, fluent API. This SDK makes it easy to create structured JSON responses for Claude Code hooks using an expressive, chainable interface.</li>
|
||||
<li><a href="https://github.com/johnlindquist/claude-hooks">claude-hooks</a> by <a href="https://github.com/johnlindquist">John Lindquist</a> - A TypeScript-based system for configuring and customizing Claude Code hooks with a powerful and flexible interface.</li>
|
||||
<li><a href="https://github.com/ctoth/claudio">Claudio</a> by <a href="https://github.com/ctoth">Christopher Toth</a> - A no-frills little library that adds delightful OS-native sounds to Claude Code via simple hooks. It really sparks joy.</li>
|
||||
<li><a href="https://github.com/nizos/tdd-guard">TDD Guard</a> by <a href="https://github.com/nizos">Nizar Selander</a> - A hooks-driven system that monitors file operations in real-time and blocks changes that violate TDD principles.</li>
|
||||
<li><a href="https://github.com/bartolli/claude-code-typescript-hooks">TypeScript Quality Hooks</a> by <a href="https://github.com/bartolli">bartolli</a> - Quality check hook for Node.js TypeScript projects with TypeScript compilation. ESLint auto-fixing, and Prettier formatting. Uses SHA256 config caching for < 5ms validation performance during real-time editing.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Slash-Commands 🔪</h2><a id="user-content-slash-commands-" class="anchor" aria-label="Permalink: Slash-Commands 🔪" href="#slash-commands-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">"Slash Commands are customized, carefully refined prompts that control Claude's behavior in order to perform a specific task"</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-5" class="anchor" aria-label="Permalink: General" href="#general-5"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/omril321/automated-notebooklm/blob/main/.claude/commands/create-hook.md">/create-hook</a> by <a href="https://github.com/omril321">Omri Lavi</a> - Slash command for hook creation - intelligently prompts you through the creation process with smart suggestions based on your project setup (TS, Prettier, ESLint...).</li>
|
||||
<li><a href="https://github.com/danielrosehill/Claude-Code-Linux-Desktop-Slash-Commands">/linux-desktop-slash-commands</a> by <a href="https://github.com/danielrosehill">Daniel Rosehill</a> - A library of slash commands intended specifically to facilitate common and advanced operations on Linux desktop environments (although many would also be useful on Linux servers). Command groups include hardware benchmarking, filesystem organisation, and security posture validation.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Version Control & Git</h3><a id="user-content-version-control--git" class="anchor" aria-label="Permalink: Version Control & Git" href="#version-control--git"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/jerseycheese/Narraitor/blob/feature/issue-227-ai-suggestions/.claude/commands/analyze-issue.md">/analyze-issue</a> by <a href="https://github.com/jerseycheese">jerseycheese</a> - Fetches GitHub issue details to create comprehensive implementation specifications, analyzing requirements and planning structured approach with clear implementation steps.</li>
|
||||
<li><a href="https://github.com/evmts/tevm-monorepo/blob/main/.claude/commands/commit.md">/commit</a> by <a href="https://github.com/evmts">evmts</a> - Creates git commits using conventional commit format with appropriate emojis, following project standards and creating descriptive messages that explain the purpose of changes.</li>
|
||||
<li><a href="https://github.com/steadycursor/steadystart/blob/main/.claude/commands/2-commit-fast.md">/commit-fast</a> by <a href="https://github.com/steadycursor">steadycursor</a> - Automates git commit process by selecting the first suggested message, generating structured commits with consistent formatting while skipping manual confirmation and removing Claude co-Contributorship footer.</li>
|
||||
<li><a href="https://github.com/toyamarinyon/giselle/blob/main/.claude/commands/create-pr.md">/create-pr</a> by <a href="https://github.com/toyamarinyon">toyamarinyon</a> - Streamlines pull request creation by handling the entire workflow: creating a new branch, committing changes, formatting modified files with Biome, and submitting the PR.</li>
|
||||
<li><a href="https://github.com/liam-hq/liam/blob/main/.claude/commands/create-pull-request.md">/create-pull-request</a> by <a href="https://github.com/liam-hq">liam-hq</a> - Provides comprehensive PR creation guidance with GitHub CLI, enforcing title conventions, following template structure, and offering concrete command examples with best practices.</li>
|
||||
<li><a href="https://github.com/evmts/tevm-monorepo/blob/main/.claude/commands/create-worktrees.md">/create-worktrees</a> by <a href="https://github.com/evmts">evmts</a> - Creates git worktrees for all open PRs or specific branches, handling branches with slashes, cleaning up stale worktrees, and supporting custom branch creation for development.</li>
|
||||
<li><a href="https://github.com/jeremymailen/kotlinter-gradle/blob/master/.claude/commands/fix-github-issue.md">/fix-github-issue</a> by <a href="https://github.com/jeremymailen">jeremymailen</a> - Analyzes and fixes GitHub issues using a structured approach with GitHub CLI for issue details, implementing necessary code changes, running tests, and creating proper commit messages.</li>
|
||||
<li><a href="https://github.com/metabase/metabase/blob/master/.claude/commands/fix-issue.md">/fix-issue</a> by <a href="https://github.com/metabase">metabase</a> - Addresses GitHub issues by taking issue number as parameter, analyzing context, implementing solution, and testing/validating the fix for proper integration.</li>
|
||||
<li><a href="https://github.com/metabase/metabase/blob/master/.claude/commands/fix-pr.md">/fix-pr</a> by <a href="https://github.com/metabase">metabase</a> - Fetches and fixes unresolved PR comments by automatically retrieving feedback, addressing reviewer concerns, making targeted code improvements, and streamlining the review process.</li>
|
||||
<li><a href="https://github.com/evmts/tevm-monorepo/blob/main/.claude/commands/husky.md">/husky</a> by <a href="https://github.com/evmts">evmts</a> - Sets up and manages Husky Git hooks by configuring pre-commit hooks, establishing commit message standards, integrating with linting tools, and ensuring code quality on commits.</li>
|
||||
<li><a href="https://github.com/giselles-ai/giselle/blob/main/.claude/commands/update-branch-name.md">/update-branch-name</a> by <a href="https://github.com/giselles-ai">giselles-ai</a> - Updates branch names with proper prefixes and formats, enforcing naming conventions, supporting semantic prefixes, and managing remote branch updates.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Code Analysis & Testing</h3><a id="user-content-code-analysis--testing" class="anchor" aria-label="Permalink: Code Analysis & Testing" href="#code-analysis--testing"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/rygwdn/slack-tools/blob/main/.claude/commands/check.md">/check</a> by <a href="https://github.com/rygwdn">rygwdn</a> - Performs comprehensive code quality and security checks, featuring static analysis integration, security vulnerability scanning, code style enforcement, and detailed reporting.</li>
|
||||
<li><a href="https://github.com/kingler/n8n_agent/blob/main/.claude/commands/code_analysis.md">/code_analysis</a> by <a href="https://github.com/kingler">kingler</a> - Provides a menu of advanced code analysis commands for deep inspection, including knowledge graph generation, optimization suggestions, and quality evaluation.</li>
|
||||
<li><a href="https://github.com/to4iki/ai-project-rules/blob/main/.claude/commands/optimize.md">/optimize</a> by <a href="https://github.com/to4iki">to4iki</a> - Analyzes code performance to identify bottlenecks, proposing concrete optimizations with implementation guidance for improved application performance.</li>
|
||||
<li><a href="https://github.com/rzykov/metabase/blob/master/.claude/commands/repro-issue.md">/repro-issue</a> by <a href="https://github.com/rzykov">rzykov</a> - Creates reproducible test cases for GitHub issues, ensuring tests fail reliably and documenting clear reproduction steps for developers.</li>
|
||||
<li><a href="https://github.com/zscott/pane/blob/main/.claude/commands/tdd.md">/tdd</a> by <a href="https://github.com/zscott">zscott</a> - Guides development using Test-Driven Development principles, enforcing Red-Green-Refactor discipline, integrating with git workflow, and managing PR creation.</li>
|
||||
<li><a href="https://github.com/jerseycheese/Narraitor/blob/feature/issue-227-ai-suggestions/.claude/commands/tdd-implement.md">/tdd-implement</a> by <a href="https://github.com/jerseycheese">jerseycheese</a> - Implements Test-Driven Development by analyzing feature requirements, creating tests first (red), implementing minimal passing code (green), and refactoring while maintaining tests.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Context Loading & Priming</h3><a id="user-content-context-loading--priming" class="anchor" aria-label="Permalink: Context Loading & Priming" href="#context-loading--priming"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/elizaOS/elizaos.github.io/blob/main/.claude/commands/context-prime.md">/context-prime</a> by <a href="https://github.com/elizaOS">elizaOS</a> - Primes Claude with comprehensive project understanding by loading repository structure, setting development context, establishing project goals, and defining collaboration parameters.</li>
|
||||
<li><a href="https://github.com/okuvshynov/cubestat/blob/main/.claude/commands/initref.md">/initref</a> by <a href="https://github.com/okuvshynov">okuvshynov</a> - Initializes reference documentation structure with standard doc templates, API reference setup, documentation conventions, and placeholder content generation.</li>
|
||||
<li><a href="https://github.com/ethpandaops/xatu-data/blob/master/.claude/commands/load-llms-txt.md">/load-llms-txt</a> by <a href="https://github.com/ethpandaops">ethpandaops</a> - Loads LLM configuration files to context, importing specific terminology, model configurations, and establishing baseline terminology for AI discussions.</li>
|
||||
<li><a href="https://github.com/Mjvolk3/torchcell/blob/main/.claude/commands/load_coo_context.md">/load_coo_context</a> by <a href="https://github.com/Mjvolk3">Mjvolk3</a> - References specific files for sparse matrix operations, explains transform usage, compares with previous approaches, and sets data formatting context for development.</li>
|
||||
<li><a href="https://github.com/Mjvolk3/torchcell/blob/main/.claude/commands/load_dango_pipeline.md">/load_dango_pipeline</a> by <a href="https://github.com/Mjvolk3">Mjvolk3</a> - Sets context for model training by referencing pipeline files, establishing working context, and preparing for pipeline work with relevant documentation.</li>
|
||||
<li><a href="https://github.com/yzyydev/AI-Engineering-Structure/blob/main/.claude/commands/prime.md">/prime</a> by <a href="https://github.com/yzyydev">yzyydev</a> - Sets up initial project context by viewing directory structure and reading key files, creating standardized context with directory visualization and key documentation focus.</li>
|
||||
<li><a href="https://github.com/ddisisto/si/blob/main/.claude/commands/rsi.md">/rsi</a> by <a href="https://github.com/ddisisto">ddisisto</a> - Reads all commands and key project files to optimize AI-assisted development by streamlining the process, loading command context, and setting up for better development workflow.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Documentation & Changelogs</h3><a id="user-content-documentation--changelogs" class="anchor" aria-label="Permalink: Documentation & Changelogs" href="#documentation--changelogs"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/berrydev-ai/blockdoc-python/blob/main/.claude/commands/add-to-changelog.md">/add-to-changelog</a> by <a href="https://github.com/berrydev-ai">berrydev-ai</a> - Adds new entries to changelog files while maintaining format consistency, properly documenting changes, and following established project standards for version tracking.</li>
|
||||
<li><a href="https://github.com/jerseycheese/Narraitor/blob/feature/issue-227-ai-suggestions/.claude/commands/create-docs.md">/create-docs</a> by <a href="https://github.com/jerseycheese">jerseycheese</a> - Analyzes code structure and purpose to create comprehensive documentation detailing inputs/outputs, behavior, user interaction flows, and edge cases with error handling.</li>
|
||||
<li><a href="https://github.com/slunsford/coffee-analytics/blob/main/.claude/commands/docs.md">/docs</a> by <a href="https://github.com/slunsford">slunsford</a> - Generates comprehensive documentation that follows project structure, documenting APIs and usage patterns with consistent formatting for better user understanding.</li>
|
||||
<li><a href="https://github.com/hackdays-io/toban-contribution-viewer/blob/main/.claude/commands/explain-issue-fix.md">/explain-issue-fix</a> by <a href="https://github.com/hackdays-io">hackdays-io</a> - Documents solution approaches for GitHub issues, explaining technical decisions, detailing challenges overcome, and providing implementation context for better understanding.</li>
|
||||
<li><a href="https://github.com/Consiliency/Flutter-Structurizr/blob/main/.claude/commands/update-docs.md">/update-docs</a> by <a href="https://github.com/Consiliency">Consiliency</a> - Reviews current documentation status, updates implementation progress, reviews phase documents, and maintains documentation consistency across the project.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">CI / Deployment</h3><a id="user-content-ci--deployment" class="anchor" aria-label="Permalink: CI / Deployment" href="#ci--deployment"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/kelp/webdown/blob/main/.claude/commands/release.md">/release</a> by <a href="https://github.com/kelp">kelp</a> - Manages software releases by updating changelogs, reviewing README changes, evaluating version increments, and documenting release changes for better version tracking.</li>
|
||||
<li><a href="https://github.com/hackdays-io/toban-contribution-viewer/blob/main/.claude/commands/run-ci.md">/run-ci</a> by <a href="https://github.com/hackdays-io">hackdays-io</a> - Activates virtual environments, runs CI-compatible check scripts, iteratively fixes errors, and ensures all tests pass before completion.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Project & Task Management</h3><a id="user-content-project--task-management" class="anchor" aria-label="Permalink: Project & Task Management" href="#project--task-management"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/scopecraft/command/blob/main/.claude/commands/create-command.md">/create-command</a> by <a href="https://github.com/scopecraft">scopecraft</a> - Guides Claude through creating new custom commands with proper structure by analyzing requirements, templating commands by category, enforcing command standards, and creating supporting documentation.</li>
|
||||
<li><a href="https://github.com/taddyorg/inkverse/blob/main/.claude/commands/create-jtbd.md">/create-jtbd</a> by <a href="https://github.com/taddyorg">taddyorg</a> - Creates Jobs-to-be-Done frameworks that outline user needs with structured format, focusing on specific user problems and organizing by job categories for product development.</li>
|
||||
<li><a href="https://github.com/taddyorg/inkverse/blob/main/.claude/commands/create-prd.md">/create-prd</a> by <a href="https://github.com/taddyorg">taddyorg</a> - Generates comprehensive product requirement documents outlining detailed specifications, requirements, and features following standardized document structure and format.</li>
|
||||
<li><a href="https://github.com/Wirasm/claudecode-utils/blob/main/.claude/commands/create-prp.md">/create-prp</a> by <a href="https://github.com/Wirasm">Wirasm</a> - Creates product requirement plans by reading PRP methodology, following template structure, creating comprehensive requirements, and structuring product definitions for development.</li>
|
||||
<li><a href="https://github.com/jerseycheese/Narraitor/blob/feature/issue-227-ai-suggestions/.claude/commands/do-issue.md">/do-issue</a> by <a href="https://github.com/jerseycheese">jerseycheese</a> - Implements GitHub issues with manual review points, following a structured approach with issue number parameter and offering alternative automated mode for efficiency.</li>
|
||||
<li><a href="https://github.com/disler/just-prompt/blob/main/.claude/commands/project_hello_w_name.md">/project_hello_w_name</a> by <a href="https://github.com/disler">disler</a> - Creates customizable greeting components with name input, demonstrating argument passing, component reusability, state management, and user input handling.</li>
|
||||
<li><a href="https://github.com/chrisleyva/todo-slash-command/blob/main/todo.md">/todo</a> by <a href="https://github.com/chrisleyva">chrisleyva</a> - A convenient command to quickly manage project todo items without leaving the Claude Code interface, featuring due dates, sorting, task prioritization, and comprehensive todo list management.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Miscellaneous</h3><a id="user-content-miscellaneous" class="anchor" aria-label="Permalink: Miscellaneous" href="#miscellaneous"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/Mjvolk3/torchcell/blob/main/.claude/commands/fixing_go_in_graph.md">/fixing_go_in_graph</a> by <a href="https://github.com/Mjvolk3">Mjvolk3</a> - Focuses on Gene Ontology annotation integration in graph databases, handling multiple data sources, addressing graph representation issues, and ensuring correct data incorporation.</li>
|
||||
<li><a href="https://github.com/GaloyMoney/lana-bank/blob/main/.claude/commands/mermaid.md">/mermaid</a> by <a href="https://github.com/GaloyMoney">GaloyMoney</a> - Generates Mermaid diagrams from SQL schema files, creating entity relationship diagrams with table properties, validating diagram compilation, and ensuring complete entity coverage.</li>
|
||||
<li><a href="https://github.com/Mjvolk3/torchcell/blob/main/.claude/commands/review_dcell_model.md">/review_dcell_model</a> by <a href="https://github.com/Mjvolk3">Mjvolk3</a> - Reviews old Dcell implementation files, comparing with newer Dango model, noting changes over time, and analyzing refactoring approaches for better code organization.</li>
|
||||
<li><a href="https://github.com/zuplo/docs/blob/main/.claude/commands/use-stepper.md">/use-stepper</a> by <a href="https://github.com/zuplo">zuplo</a> - Reformats documentation to use React Stepper component, transforming heading formats, applying proper indentation, and maintaining markdown compatibility with admonition formatting.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">CLAUDE.md Files 📂</h2><a id="user-content-claudemd-files-" class="anchor" aria-label="Permalink: CLAUDE.md Files 📂" href="#claudemd-files-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto"><code>CLAUDE.md</code> files are files that contain important guidelines and context-specific information or instructions that help Claude Code to better understand your project and your coding standards</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Language-Specific</h3><a id="user-content-language-specific" class="anchor" aria-label="Permalink: Language-Specific" href="#language-specific"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/didalgolab/ai-intellij-plugin/blob/main/CLAUDE.md">AI IntelliJ Plugin</a> by <a href="https://github.com/didalgolab">didalgolab</a> - Provides comprehensive Gradle commands for IntelliJ plugin development with platform-specific coding patterns, detailed package structure guidelines, and clear internationalization standards.</li>
|
||||
<li><a href="https://github.com/alexei-led/aws-mcp-server/blob/main/CLAUDE.md">AWS MCP Server</a> by <a href="https://github.com/alexei-led">alexei-led</a> - Features multiple Python environment setup options with detailed code style guidelines, comprehensive error handling recommendations, and security considerations for AWS CLI interactions.</li>
|
||||
<li><a href="https://github.com/touchlab/DroidconKotlin/blob/main/CLAUDE.md">DroidconKotlin</a> by <a href="https://github.com/touchlab">touchlab</a> - Delivers comprehensive Gradle commands for cross-platform Kotlin Multiplatform development with clear module structure and practical guidance for dependency injection.</li>
|
||||
<li><a href="https://github.com/hesreallyhim/awesome-claude-code/blob/main/resources/claude.md-files/EDSL/CLAUDE.md">EDSL</a> by <a href="https://github.com/expectedparrot">expectedparrot</a> - Offers detailed build and test commands with strict code style enforcement, comprehensive testing requirements, and standardized development workflow using Black and mypy. <em>(Removed from origin)</em></li>
|
||||
<li><a href="https://github.com/giselles-ai/giselle/blob/main/CLAUDE.md">Giselle</a> by <a href="https://github.com/giselles-ai">giselles-ai</a> - Provides detailed build and test commands using pnpm and Vitest with strict code formatting requirements and comprehensive naming conventions for code consistency.</li>
|
||||
<li><a href="https://github.com/hashintel/hash/blob/main/CLAUDE.md">HASH</a> by <a href="https://github.com/hashintel">hashintel</a> - Features comprehensive repository structure breakdown with strong emphasis on coding standards, detailed Rust documentation guidelines, and systematic PR review process.</li>
|
||||
<li><a href="https://github.com/inkline/inkline/blob/main/CLAUDE.md">Inkline</a> by <a href="https://github.com/inkline">inkline</a> - Structures development workflow using pnpm with emphasis on TypeScript and Vue 3 Composition API, detailed component creation process, and comprehensive testing recommendations.</li>
|
||||
<li><a href="https://github.com/mattgodbolt/jsbeeb/blob/main/CLAUDE.md">JSBeeb</a> by <a href="https://github.com/mattgodbolt">mattgodbolt</a> - Provides development guide for JavaScript BBC Micro emulator with build and testing instructions, architecture documentation, and debugging workflows.</li>
|
||||
<li><a href="https://github.com/LamoomAI/lamoom-python/blob/main/CLAUDE.md">Lamoom Python</a> by <a href="https://github.com/LamoomAI">LamoomAI</a> - Serves as reference for production prompt engineering library with load balancing of AI Models, API documentation, and usage patterns with examples.</li>
|
||||
<li><a href="https://github.com/langchain-ai/langgraphjs/blob/main/CLAUDE.md">LangGraphJS</a> by <a href="https://github.com/langchain-ai">langchain-ai</a> - Offers comprehensive build and test commands with detailed TypeScript style guidelines, layered library architecture, and monorepo structure using yarn workspaces.</li>
|
||||
<li><a href="https://github.com/metabase/metabase/blob/master/CLAUDE.md">Metabase</a> by <a href="https://github.com/metabase">metabase</a> - Details workflow for REPL-driven development in Clojure/ClojureScript with emphasis on incremental development, testing, and step-by-step approach for feature implementation.</li>
|
||||
<li><a href="https://github.com/sgcarstrends/backend/blob/main/CLAUDE.md">SG Cars Trends Backend</a> by <a href="https://github.com/sgcarstrends">sgcarstrends</a> - Provides comprehensive structure for TypeScript monorepo projects with detailed commands for development, testing, deployment, and AWS/Cloudflare integration.</li>
|
||||
<li><a href="https://github.com/spylang/spy/blob/main/CLAUDE.md">SPy</a> by <a href="https://github.com/spylang">spylang</a> - Enforces strict coding conventions with comprehensive testing guidelines, multiple code compilation options, and backend-specific test decorators for targeted filtering.</li>
|
||||
<li><a href="https://github.com/KarpelesLab/tpl/blob/master/CLAUDE.md">TPL</a> by <a href="https://github.com/KarpelesLab">KarpelesLab</a> - Details Go project conventions with comprehensive error handling recommendations, table-driven testing approach guidelines, and modernization suggestions for latest Go features.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Domain-Specific</h3><a id="user-content-domain-specific" class="anchor" aria-label="Permalink: Domain-Specific" href="#domain-specific"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/Layr-Labs/avs-vibe-developer-guide/blob/master/CLAUDE.md">AVS Vibe Developer Guide</a> by <a href="https://github.com/Layr-Labs">Layr-Labs</a> - Structures AI-assisted EigenLayer AVS development workflow with consistent naming conventions for prompt files and established terminology standards for blockchain concepts.</li>
|
||||
<li><a href="https://github.com/CommE2E/comm/blob/master/CLAUDE.md">Comm</a> by <a href="https://github.com/CommE2E">CommE2E</a> - Serves as a development reference for E2E-encrypted messaging applications with code organization architecture, security implementation details, and testing procedures.</li>
|
||||
<li><a href="https://github.com/badass-courses/course-builder/blob/main/CLAUDE.md">Course Builder</a> by <a href="https://github.com/badass-courses">badass-courses</a> - Enables real-time multiplayer capabilities for collaborative course creation with diverse tech stack integration and monorepo architecture using Turborepo.</li>
|
||||
<li><a href="https://github.com/eastlondoner/cursor-tools/blob/main/CLAUDE.md">Cursor Tools</a> by <a href="https://github.com/eastlondoner">eastlondoner</a> - Creates a versatile AI command interface supporting multiple providers and models with flexible command options and browser automation through "Stagehand" feature.</li>
|
||||
<li><a href="https://github.com/soramimi/Guitar/blob/master/CLAUDE.md">Guitar</a> by <a href="https://github.com/soramimi">soramimi</a> - Serves as development guide for Guitar Git GUI Client with build commands for various platforms, code style guidelines for contributing, and project structure explanation.</li>
|
||||
<li><a href="https://github.com/Fimeg/NetworkChronicles/blob/legacy-v1/CLAUDE.md">Network Chronicles</a> by <a href="https://github.com/Fimeg">Fimeg</a> - Presents detailed implementation plan for AI-driven game characters with technical specifications for LLM integration, character guidelines, and service discovery mechanics.</li>
|
||||
<li><a href="https://github.com/ParetoSecurity/pareto-mac/blob/main/CLAUDE.md">Pareto Mac</a> by <a href="https://github.com/ParetoSecurity">ParetoSecurity</a> - Serves as development guide for Mac security audit tool with build instructions, contribution guidelines, testing procedures, and workflow documentation.</li>
|
||||
<li><a href="https://github.com/steadycursor/steadystart/blob/main/CLAUDE.md">SteadyStart</a> by <a href="https://github.com/steadycursor">steadycursor</a> - Clear and direct instructives about style, permissions, Claude's "role", communications, and documentation of Claude Code sessions for other team members to stay abreast.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">Project Scaffolding & MCP</h3><a id="user-content-project-scaffolding--mcp" class="anchor" aria-label="Permalink: Project Scaffolding & MCP" href="#project-scaffolding--mcp"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/basicmachines-co/basic-memory/blob/main/CLAUDE.md">Basic Memory</a> by <a href="https://github.com/basicmachines-co">basicmachines-co</a> - Presents an innovative AI-human collaboration framework with Model Context Protocol for bidirectional LLM-markdown communication and flexible knowledge structure for complex projects.</li>
|
||||
<li><a href="https://github.com/grahama1970/claude-code-mcp-enhanced/blob/main/CLAUDE.md">claude-code-mcp-enhanced</a> by <a href="https://github.com/grahama1970">grahama1970</a> - Provides detailed and emphatic instructions for Claude to follow as a coding agent, with testing guidance, code examples, and compliance checks.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Alternative Clients 📱</h2><a id="user-content-alternative-clients-" class="anchor" aria-label="Permalink: Alternative Clients 📱" href="#alternative-clients-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">Alternative Clients are alternative UIs and front-ends for interacting with Claude Code, either on mobile or on the desktop.</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-6" class="anchor" aria-label="Permalink: General" href="#general-6"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://github.com/opactorai/Claudable">Claudable</a> by <a href="https://www.linkedin.com/in/seongil-park/" rel="nofollow">Ethan Park</a> - Claudable is an open-source web builder that leverages local CLI agents, such as Claude Code and Cursor Agent, to build and deploy products effortlessly.</li>
|
||||
<li><a href="https://github.com/omnara-ai/omnara">Omnara</a> by <a href="https://github.com/ishaansehgal99">Ishaan Sehgal</a> - A command center for AI agents that syncs Claude Code sessions across terminal, web, and mobile. Allows for remote monitoring, human-in-the-loop interaction, and team collaboration.</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Official Documentation 🏛️</h2><a id="user-content-official-documentation-️" class="anchor" aria-label="Permalink: Official Documentation 🏛️" href="#official-documentation-️"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<blockquote>
|
||||
<p dir="auto">Links to some of Anthropic's terrific documentation and resources regarding Claude Code</p>
|
||||
</blockquote>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto">General</h3><a id="user-content-general-7" class="anchor" aria-label="Permalink: General" href="#general-7"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<ul dir="auto">
|
||||
<li><a href="https://docs.claude.com/en/home" rel="nofollow">Anthropic Documentation</a> by <a href="https://github.com/anthropics">Anthropic</a> - The official documentation for Claude Code, including installation instructions, usage guidelines, API references, tutorials, examples, loads of information that I won't list individually. Like Claude Code, the documentation is frequently updated.</li>
|
||||
<li><a href="https://github.com/anthropics/claude-quickstarts">Anthropic Quickstarts</a> by <a href="https://github.com/anthropics">Anthropic</a> - Offers comprehensive development guides for three distinct AI-powered demo projects with standardized workflows, strict code style guidelines, and containerization instructions.</li>
|
||||
<li><a href="https://github.com/anthropics/claude-code-action/tree/main/examples">Claude Code GitHub Actions</a> by <a href="https://github.com/anthropics">Anthropic</a> - Official GitHub Actions integration for Claude Code with examples and documentation for automating AI-powered workflows in CI/CD pipelines.</li>
|
||||
</ul>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Contributing <a href="#awesome-claude-code">🔝</a></h2><a id="user-content-contributing-" class="anchor" aria-label="Permalink: Contributing 🔝" href="#contributing-"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<div class="markdown-heading" dir="auto"><h3 tabindex="-1" class="heading-element" dir="auto"><strong><a href="https://github.com/hesreallyhim/awesome-claude-code/issues/new?template=recommend-resource.yml">Recommend a new resource here!</a></strong></h3><a id="user-content-recommend-a-new-resource-here" class="anchor" aria-label="Permalink: Recommend a new resource here!" href="#recommend-a-new-resource-here"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<p dir="auto">Recommending a resource for the list is very simple, and the automated system handles everything for you. Please do not open a PR to submit a recommendation - the only person who is allowed to submit PRs to this repo is Claude.</p>
|
||||
<p dir="auto">Make sure that you have read the CONTRIBUTING.md document and CODE_OF_CONDUCT.md before you submit a recommendation.</p>
|
||||
<p dir="auto">For suggestions about the repository itself, please <a href="https://github.com/hesreallyhim/awesome-claude-code/issues/new?template=repository-enhancement.yml">open a repository enhancement issue</a>.</p>
|
||||
<p dir="auto">This project is released with a Code of Conduct. By participating, you agree to abide by its terms. And although I take strong measures to uphold the quality and safety of this list, I take no responsibility or liability for anything that might happen as a result of these third-party resources.</p>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Growing thanks to you</h2><a id="user-content-growing-thanks-to-you" class="anchor" aria-label="Permalink: Growing thanks to you" href="#growing-thanks-to-you"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<p dir="auto"><a href="https://starchart.cc/hesreallyhim/awesome-claude-code" rel="nofollow"><img src="https://camo.githubusercontent.com/ef0d0c39da6555d5ae7a097957ad888572e4253d166c16db130e7028ba06ee5e/68747470733a2f2f7374617263686172742e63632f6865737265616c6c7968696d2f617765736f6d652d636c617564652d636f64652e7376673f76617269616e743d6164617074697665" alt="Stargazers over time" data-canonical-src="https://starchart.cc/hesreallyhim/awesome-claude-code.svg?variant=adaptive" style="max-width: 100%;"></a></p>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">License</h2><a id="user-content-license" class="anchor" aria-label="Permalink: License" href="#license"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a></div>
|
||||
<p dir="auto">This list is licensed under <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/" rel="nofollow">Creative Commons CC BY-NC-ND 4.0</a> - this means you are welcome to fork, clone, copy and redistribute the list, provided you include appropriate attribution; however you are not permitted to distribute any modified versions or to use it for any commercial purposes. This is to prevent disregard for the licenses of the authors whose resources are listed here. Please note that all resources included in this list have their own license terms.</p>
|
||||
</article>
|
||||
1381
.agent/knowledge/awesome_claude/tests/fixtures/github-html/classic-non-root.html
vendored
Normal file
1381
.agent/knowledge/awesome_claude/tests/fixtures/github-html/classic-non-root.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1472
.agent/knowledge/awesome_claude/tests/fixtures/github-html/extra-non-root.html
vendored
Normal file
1472
.agent/knowledge/awesome_claude/tests/fixtures/github-html/extra-non-root.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
.agent/knowledge/awesome_claude/tests/fixtures/github-html/flat-non-root.html
vendored
Normal file
9
.agent/knowledge/awesome_claude/tests/fixtures/github-html/flat-non-root.html
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- Minimal fixture containing only heading anchor IDs for TOC validation -->
|
||||
<!-- Generated from GitHub-rendered README_FLAT_ALL_AZ.md -->
|
||||
<!-- FLAT style has no TOC anchors; this fixture validates GitHub's anchor generation -->
|
||||
<article class="markdown-body entry-content container-lg" itemprop="text">
|
||||
<div class="markdown-heading" dir="auto"><h3 align="center" tabindex="-1" class="heading-element" dir="auto">Pick Your Style:</h3><a id="user-content-pick-your-style" class="anchor" href="#pick-your-style"></a></div>
|
||||
<div class="markdown-heading" dir="auto"><h1 tabindex="-1" class="heading-element" dir="auto">Awesome Claude Code (Flat)</h1><a id="user-content-awesome-claude-code-flat" class="anchor" href="#awesome-claude-code-flat"></a></div>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Sort By:</h2><a id="user-content-sort-by" class="anchor" href="#sort-by"></a></div>
|
||||
<div class="markdown-heading" dir="auto"><h2 tabindex="-1" class="heading-element" dir="auto">Resources</h2><a id="user-content-resources" class="anchor" href="#resources"></a></div>
|
||||
</article>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"description": "Informal submission using 'Resource Recommendation' pattern with markdown headers (no labels). Uses '### Category' not 'Category:' so fewer template signals - triggers MEDIUM confidence (warn).",
|
||||
"original_labels": [],
|
||||
"workflow_runs_detection": true,
|
||||
"title": "Resource Recommendation: awesome-resource",
|
||||
"body": "## Resource Recommendation\n\n### Resource Name\nawesome-resource\n\n### Resource URL\nhttps://github.com/username/awesome-resource\n\n### Category\nSlash-Commands > Project & Task Management\n\n### About\nA plugin that generates documents from conversation context. Works standalone without external templates.\n\n### Features\n- Works immediately after install\n- Synthesizes requirements from conversation\n- Generates standard sections\n- Accepts optional arguments\n\n### Author\nUsername (@username)",
|
||||
"expected_action_if_evaluated_by_workflow": "warn"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"description": "Informal submission with custom structure using **Field:** pattern (no labels) - triggers HIGH confidence (close).",
|
||||
"original_labels": [],
|
||||
"workflow_runs_detection": true,
|
||||
"title": "Add: awesome-resource",
|
||||
"body": "## Resource Information\n\n**Name:** awesome-resource\n\n**URL:** https://github.com/username/awesome-resource\n\n**Author:** Username (https://github.com/username)\n\n**Category:** Agent Skills > General\n\n**Description:** This is an awesome resource and I think it would be great to include. It has full testing and I recommend it. A curated collection of plugins with enforcement hooks, desktop notifications, and usage monitoring.\n\n## Why it fits\n\nA well-organized plugin collection with:\n- **linter:** Enforcement hooks for security, cleanup, automatic linting, version syncing\n- **notifier:** Desktop notifications showing current status\n- **tracker:** Real-time usage display with progress bars\n- Comprehensive documentation\n\n## Additional context\n\nThe repository follows best practices and is actively maintained.",
|
||||
"expected_action_if_evaluated_by_workflow": "close"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"description": "Issue submitted using proper template (has resource-submission label). Script outputs 'close' but workflow job never runs due to label condition.",
|
||||
"original_labels": [
|
||||
"resource-submission",
|
||||
"validation-passed"
|
||||
],
|
||||
"workflow_runs_detection": false,
|
||||
"title": "[Resource]: awesome-resource",
|
||||
"body": "### Display Name\n\nawesome-resource\n\n### Category\n\nTooling\n\n### Sub-Category\n\nGeneral\n\n### Primary Link\n\nhttps://github.com/username/awesome-resource\n\n### Author Name\n\nusername\n\n### Author Link\n\nhttps://github.com/username\n\n### License\n\nMIT\n\n### Other License\n\n_No response_\n\n### Description\n\nThis is an awesome resource and I think it would be great to include. It has full testing and I recommend it. CLI tool that converts conversation transcripts into formatted output.\n\n### Validate Claims\n\n```\n# Run it in any directory\nuvx awesome-resource show\n\n# Export to HTML\nuvx awesome-resource show --format html --output test.html\n```\n\n### Specific Task(s)\n\n_No response_\n\n### Specific Prompt(s)\n\n_No response_\n\n### Additional Comments\n\n_No response_\n\n### Recommendation Checklist\n\n- [x] I have checked that this resource hasn't already been submitted\n- [x] My resource provides genuine value to Claude Code users, and any risks are clearly stated\n- [x] All provided links are working and publicly accessible\n- [x] I am submitting only ONE resource in this issue\n- [x] I understand that low-quality or duplicate submissions may be rejected",
|
||||
"expected_action_if_evaluated_by_workflow": "close"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"description": "Issue submitted using proper template (has resource-submission label). Script outputs 'close' but workflow job never runs due to label condition.",
|
||||
"original_labels": [
|
||||
"resource-submission",
|
||||
"validation-passed"
|
||||
],
|
||||
"workflow_runs_detection": false,
|
||||
"title": "[Resource]: awesome-resource",
|
||||
"body": "### Display Name\n\nawesome-resource\n\n### Category\n\nTooling\n\n### Sub-Category\n\nGeneral\n\n### Primary Link\n\nhttps://github.com/username/awesome-resource\n\n### Author Name\n\nusername\n\n### Author Link\n\nhttps://github.com/username\n\n### License\n\nMIT\n\n### Other License\n\n_No response_\n\n### Description\n\nThis is an awesome resource and I think it would be great to include. It has full testing and I recommend it. Terminal session manager for running multiple coding sessions in parallel.\n\n### Validate Claims\n\nInstall via the readme instructions, then open the application and observe the features in action.\n\n### Specific Task(s)\n\n_No response_\n\n### Specific Prompt(s)\n\n_No response_\n\n### Additional Comments\n\n_No response_\n\n### Recommendation Checklist\n\n- [x] I have checked that this resource hasn't already been submitted\n- [x] My resource provides genuine value to Claude Code users, and any risks are clearly stated\n- [x] All provided links are working and publicly accessible\n- [x] I am submitting only ONE resource in this issue\n- [x] I understand that low-quality or duplicate submissions may be rejected",
|
||||
"expected_action_if_evaluated_by_workflow": "close"
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Temporary test to verify the auto-locking behavior of the refactored override system.
|
||||
This test confirms that setting a field in overrides automatically locks it from validation updates.
|
||||
|
||||
Context: Refactored the override system so that any field set in resource-overrides.yaml
|
||||
is automatically locked without needing explicit *_locked flags.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add scripts directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||||
|
||||
from scripts.validation.validate_links import apply_overrides # noqa: E402
|
||||
|
||||
|
||||
def test_auto_locking():
|
||||
"""Test that setting a field automatically locks it."""
|
||||
# Sample resource row
|
||||
row = {
|
||||
"ID": "test-123",
|
||||
"Display Name": "Test Resource",
|
||||
"License": "Apache-2.0",
|
||||
"Active": "TRUE",
|
||||
"Last Checked": "2025-01-01:12-00-00",
|
||||
}
|
||||
|
||||
# Override config without *_locked flags (new format)
|
||||
overrides = {
|
||||
"test-123": {
|
||||
"license": "MIT", # Should auto-lock
|
||||
"active": "FALSE", # Should auto-lock
|
||||
}
|
||||
}
|
||||
|
||||
# Apply overrides
|
||||
updated_row, locked_fields, skip_validation = apply_overrides(row, overrides)
|
||||
|
||||
# Verify override values were applied
|
||||
assert updated_row["License"] == "MIT", "License override not applied"
|
||||
assert updated_row["Active"] == "FALSE", "Active override not applied"
|
||||
|
||||
# Verify fields are automatically locked
|
||||
assert "license" in locked_fields, "License field not auto-locked"
|
||||
assert "active" in locked_fields, "Active field not auto-locked"
|
||||
|
||||
# Verify skip_validation is False
|
||||
assert skip_validation is False, "skip_validation should be False"
|
||||
|
||||
print("✅ Test passed: Fields are automatically locked when set in overrides")
|
||||
|
||||
|
||||
def test_skip_validation_precedence():
|
||||
"""Test that skip_validation has highest precedence."""
|
||||
row = {"ID": "test-456", "Display Name": "Skipped Resource"}
|
||||
|
||||
overrides = {"test-456": {"skip_validation": True, "license": "MIT"}}
|
||||
|
||||
updated_row, locked_fields, skip_validation = apply_overrides(row, overrides)
|
||||
|
||||
# Verify skip_validation is True
|
||||
assert skip_validation is True, "skip_validation should be True"
|
||||
|
||||
# Verify license was still applied and locked
|
||||
assert updated_row["License"] == "MIT", "License override not applied"
|
||||
assert "license" in locked_fields, "License field not auto-locked"
|
||||
|
||||
print("✅ Test passed: skip_validation has highest precedence")
|
||||
|
||||
|
||||
def test_legacy_locked_flags_ignored():
|
||||
"""Test that legacy *_locked flags are properly ignored."""
|
||||
row = {"ID": "test-789", "License": "Apache-2.0"}
|
||||
|
||||
# Override config with legacy *_locked flag (should be ignored)
|
||||
overrides = {
|
||||
"test-789": {
|
||||
"license": "MIT",
|
||||
"license_locked": True, # Legacy flag, should be ignored
|
||||
}
|
||||
}
|
||||
|
||||
updated_row, locked_fields, skip_validation = apply_overrides(row, overrides)
|
||||
|
||||
# Verify override was applied
|
||||
assert updated_row["License"] == "MIT", "License override not applied"
|
||||
|
||||
# Verify field is still auto-locked (from the field value, not the legacy flag)
|
||||
assert "license" in locked_fields, "License field not auto-locked"
|
||||
|
||||
print("✅ Test passed: Legacy *_locked flags are properly ignored")
|
||||
|
||||
|
||||
def test_notes_field_ignored():
|
||||
"""Test that notes field doesn't cause auto-locking."""
|
||||
row = {"ID": "test-notes", "License": "MIT"}
|
||||
|
||||
overrides = {"test-notes": {"notes": "This is a note"}}
|
||||
|
||||
updated_row, locked_fields, skip_validation = apply_overrides(row, overrides)
|
||||
|
||||
# Verify no fields are locked
|
||||
assert len(locked_fields) == 0, "Notes field should not cause auto-locking"
|
||||
|
||||
print("✅ Test passed: Notes field doesn't cause auto-locking")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Running override auto-lock verification tests...\n")
|
||||
|
||||
test_auto_locking()
|
||||
test_skip_validation_precedence()
|
||||
test_legacy_locked_flags_ignored()
|
||||
test_notes_field_ignored()
|
||||
|
||||
print("\n🎉 All verification tests passed!")
|
||||
print("The refactored override system is working correctly.")
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for asset token resolution behavior."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.readme.helpers.readme_paths import (
|
||||
asset_path_token,
|
||||
ensure_generated_header,
|
||||
resolve_asset_tokens,
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_asset_tokens_root(tmp_path: Path) -> None:
|
||||
content = f'<img src="{asset_path_token("logo.svg")}">'
|
||||
resolved = resolve_asset_tokens(content, tmp_path / "README.md", tmp_path)
|
||||
assert 'src="assets/logo.svg"' in resolved
|
||||
|
||||
|
||||
def test_resolve_asset_tokens_alternative(tmp_path: Path) -> None:
|
||||
content = f'<img src="{asset_path_token("logo.svg")}">'
|
||||
resolved = resolve_asset_tokens(
|
||||
content, tmp_path / "README_ALTERNATIVES" / "README_EXTRA.md", tmp_path
|
||||
)
|
||||
assert 'src="../assets/logo.svg"' in resolved
|
||||
|
||||
|
||||
def test_resolve_asset_tokens_asset_scheme(tmp_path: Path) -> None:
|
||||
content = '<img src="asset:badge.svg">'
|
||||
resolved = resolve_asset_tokens(content, tmp_path / "README.md", tmp_path)
|
||||
assert 'src="assets/badge.svg"' in resolved
|
||||
|
||||
|
||||
def test_ensure_generated_header() -> None:
|
||||
content = "Hello\n"
|
||||
updated = ensure_generated_header(content)
|
||||
assert updated.startswith("<!-- GENERATED FILE: do not edit directly -->\n")
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Security validation tests for badge notification system
|
||||
Tests that dangerous inputs are REJECTED, not sanitized
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(repo_root))
|
||||
|
||||
from scripts.badges.badge_notification_core import BadgeNotificationCore # noqa: E402
|
||||
|
||||
|
||||
def test_dangerous_input_rejection() -> None:
|
||||
"""Test that dangerous inputs are rejected, not modified"""
|
||||
print("Testing Dangerous Input Rejection...")
|
||||
|
||||
# Test cases that should be REJECTED
|
||||
dangerous_inputs = [
|
||||
("<script>alert('XSS')</script>", "HTML script tag"),
|
||||
("</textarea><script>alert('XSS')</script>", "Script with closing tag"),
|
||||
("<img src=x onerror=alert('XSS')>", "Image with onerror"),
|
||||
("<iframe src='evil.com'></iframe>", "Iframe injection"),
|
||||
("javascript:alert('XSS')", "JavaScript protocol"),
|
||||
("data:text/html,<script>alert('XSS')</script>", "Data protocol"),
|
||||
("vbscript:msgbox('XSS')", "VBScript protocol"),
|
||||
("<svg onload=alert('XSS')>", "SVG with event handler"),
|
||||
("Test onclick=alert('XSS')", "Inline event handler"),
|
||||
("file:///etc/passwd", "File protocol"),
|
||||
("Test\x00with null", "Null byte injection"),
|
||||
("Test" + chr(7) + "bell", "Control character"),
|
||||
]
|
||||
|
||||
for payload, description in dangerous_inputs:
|
||||
is_safe, reason = BadgeNotificationCore.validate_input_safety(payload, "test_field")
|
||||
assert not is_safe, f"Failed to reject: {description}"
|
||||
assert reason, f"No reason provided for rejection: {description}"
|
||||
print(f" ✓ Rejected: {description} - Reason: {reason}")
|
||||
|
||||
|
||||
def test_safe_input_acceptance() -> None:
|
||||
"""Test that legitimate inputs are accepted"""
|
||||
print("\nTesting Safe Input Acceptance...")
|
||||
|
||||
# Test cases that should be ACCEPTED
|
||||
safe_inputs = [
|
||||
("Claude Code Tools", "Normal project name"),
|
||||
("A tool for enhancing productivity", "Normal description"),
|
||||
("Project-Name_123", "Name with special chars"),
|
||||
("Version 2.0 (Beta)", "Parentheses and dots"),
|
||||
("# Best Practices Guide", "Markdown heading in plain text"),
|
||||
("Use `code` blocks", "Backticks in description"),
|
||||
("Email: user@example.com", "Email address"),
|
||||
("https://github.com/owner/repo", "GitHub URL"),
|
||||
("Line 1\nLine 2\nLine 3", "Multi-line text"),
|
||||
("Unicode: 你好 мир 🚀", "Unicode characters"),
|
||||
]
|
||||
|
||||
for payload, description in safe_inputs:
|
||||
is_safe, reason = BadgeNotificationCore.validate_input_safety(payload, "test_field")
|
||||
assert is_safe, f"Incorrectly rejected safe input: {description}. Reason: {reason}"
|
||||
print(f" ✓ Accepted: {description}")
|
||||
|
||||
|
||||
def test_length_limit_enforcement() -> None:
|
||||
"""Test that overly long inputs are rejected"""
|
||||
print("\nTesting Length Limit Enforcement...")
|
||||
|
||||
# Very long input (over 5000 chars)
|
||||
long_input = "A" * 5001
|
||||
is_safe, reason = BadgeNotificationCore.validate_input_safety(long_input, "test_field")
|
||||
assert not is_safe, "Failed to reject overly long input"
|
||||
assert "exceeds maximum length" in reason, f"Wrong rejection reason: {reason}"
|
||||
print(" ✓ Rejected input over 5000 characters")
|
||||
|
||||
# Input at the limit should be accepted
|
||||
limit_input = "A" * 5000
|
||||
is_safe, reason = BadgeNotificationCore.validate_input_safety(limit_input, "test_field")
|
||||
assert is_safe, "Incorrectly rejected input at length limit"
|
||||
print(" ✓ Accepted input at 5000 character limit")
|
||||
|
||||
|
||||
def test_case_insensitive_detection() -> None:
|
||||
"""Test that dangerous patterns are detected case-insensitively"""
|
||||
print("\nTesting Case-Insensitive Detection...")
|
||||
|
||||
case_variants = [
|
||||
("JAVASCRIPT:alert('XSS')", "Uppercase protocol"),
|
||||
("JaVaScRiPt:alert('XSS')", "Mixed case protocol"),
|
||||
("<SCRIPT>alert('XSS')</SCRIPT>", "Uppercase tags"),
|
||||
("<ScRiPt>alert('XSS')</ScRiPt>", "Mixed case tags"),
|
||||
("ONCLICK=alert('XSS')", "Uppercase event handler"),
|
||||
]
|
||||
|
||||
for payload, description in case_variants:
|
||||
is_safe, reason = BadgeNotificationCore.validate_input_safety(payload, "test_field")
|
||||
assert not is_safe, f"Failed to reject: {description}"
|
||||
print(f" ✓ Rejected: {description}")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("github_stub")
|
||||
def test_issue_creation_with_validation() -> None:
|
||||
"""Test that issue creation fails with dangerous inputs"""
|
||||
print("\nTesting Issue Creation with Validation...")
|
||||
|
||||
notifier = BadgeNotificationCore("fake_token")
|
||||
|
||||
# Test with dangerous resource name
|
||||
try:
|
||||
notifier.create_issue_body("<script>alert('XSS')</script>", "Normal description")
|
||||
raise AssertionError("Should have raised ValueError for dangerous resource name")
|
||||
except ValueError as e:
|
||||
assert "Security validation failed" in str(e)
|
||||
print(" ✓ Issue creation blocked for dangerous resource name")
|
||||
|
||||
# Test with dangerous description
|
||||
try:
|
||||
notifier.create_issue_body("Normal Name", "javascript:alert('XSS')")
|
||||
raise AssertionError("Should have raised ValueError for dangerous description")
|
||||
except ValueError as e:
|
||||
assert "Security validation failed" in str(e)
|
||||
print(" ✓ Issue creation blocked for dangerous description")
|
||||
|
||||
# Test with safe inputs (should not raise)
|
||||
try:
|
||||
body = notifier.create_issue_body("Safe Project", "A safe description")
|
||||
assert "Safe Project" in body, "Original text should be in output"
|
||||
assert "A safe description" in body, "Original description should be in output"
|
||||
print(" ✓ Issue creation allowed for safe inputs")
|
||||
except ValueError as e:
|
||||
raise AssertionError(f"Should not have raised ValueError for safe inputs: {e}") from e
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("github_stub")
|
||||
def test_notification_creation_flow() -> None:
|
||||
"""Test the full notification creation flow with validation"""
|
||||
print("\nTesting Full Notification Creation Flow...")
|
||||
|
||||
notifier = BadgeNotificationCore("fake_token")
|
||||
|
||||
# Test that dangerous inputs result in failed notification
|
||||
result = notifier.create_notification_issue(
|
||||
repo_url="https://github.com/owner/repo",
|
||||
resource_name="<script>alert('XSS')</script>",
|
||||
description="Normal description",
|
||||
)
|
||||
|
||||
assert not result["success"], "Should have failed with dangerous input"
|
||||
assert "Security validation failed" in result["message"], (
|
||||
f"Wrong error message: {result['message']}"
|
||||
)
|
||||
print(" ✓ Notification creation blocked for dangerous input")
|
||||
|
||||
|
||||
def run_all_tests() -> bool:
|
||||
"""Run all validation tests"""
|
||||
print("=" * 60)
|
||||
print("Badge Notification Validation Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
test_dangerous_input_rejection()
|
||||
test_safe_input_acceptance()
|
||||
test_length_limit_enforcement()
|
||||
test_case_insensitive_detection()
|
||||
test_issue_creation_with_validation()
|
||||
test_notification_creation_flow()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ All validation tests passed!")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
except AssertionError as e:
|
||||
print(f"\n❌ Test failed: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"\n❌ Unexpected error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
347
.agent/knowledge/awesome_claude/tests/test_category_utils.py
Normal file
347
.agent/knowledge/awesome_claude/tests/test_category_utils.py
Normal file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for the CategoryManager class.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.categories.category_utils import CategoryManager # noqa: E402
|
||||
|
||||
|
||||
def create_test_categories() -> dict[str, Any]:
|
||||
"""Create test category data."""
|
||||
return {
|
||||
"categories": [
|
||||
{
|
||||
"id": "cat1",
|
||||
"name": "Category One",
|
||||
"prefix": "c1",
|
||||
"icon": "🔵",
|
||||
"description": "First test category",
|
||||
"order": 2,
|
||||
"subcategories": [
|
||||
{"id": "sub1", "name": "Subcategory A"},
|
||||
{"id": "sub2", "name": "Subcategory B"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "cat2",
|
||||
"name": "Category Two",
|
||||
"prefix": "c2",
|
||||
"icon": "🟢",
|
||||
"description": "Second test category",
|
||||
"order": 1,
|
||||
},
|
||||
{
|
||||
"id": "cat3",
|
||||
"name": "Category Three",
|
||||
"prefix": "c3",
|
||||
"icon": "🔴",
|
||||
"description": "Third test category",
|
||||
"order": 3,
|
||||
"subcategories": [
|
||||
{"id": "sub3", "name": "Subcategory C"},
|
||||
],
|
||||
},
|
||||
],
|
||||
"toc": {
|
||||
"style": "test",
|
||||
"symbol": "►",
|
||||
"subsymbol": "▸",
|
||||
"indent": " ",
|
||||
"subindent": " ",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_get_all_categories() -> None:
|
||||
"""Test getting all category names."""
|
||||
# Create a new instance with test data
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
categories = manager.get_all_categories()
|
||||
|
||||
# Check we have the expected test categories
|
||||
assert "Category One" in categories
|
||||
assert "Category Two" in categories
|
||||
assert "Category Three" in categories
|
||||
assert len(categories) == 3
|
||||
|
||||
|
||||
def test_get_category_prefixes() -> None:
|
||||
"""Test getting category ID prefixes."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
prefixes = manager.get_category_prefixes()
|
||||
|
||||
# Check mappings from our test data
|
||||
assert prefixes["Category One"] == "c1"
|
||||
assert prefixes["Category Two"] == "c2"
|
||||
assert prefixes["Category Three"] == "c3"
|
||||
assert len(prefixes) == 3
|
||||
|
||||
|
||||
def test_get_category_by_name() -> None:
|
||||
"""Test retrieving category by name."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
# Test existing category
|
||||
cat_one = manager.get_category_by_name("Category One")
|
||||
assert cat_one is not None
|
||||
assert cat_one["id"] == "cat1"
|
||||
assert cat_one["prefix"] == "c1"
|
||||
assert cat_one["icon"] == "🔵"
|
||||
assert len(cat_one["subcategories"]) == 2
|
||||
|
||||
# Test non-existent category
|
||||
nonexistent = manager.get_category_by_name("NonExistent")
|
||||
assert nonexistent is None
|
||||
|
||||
|
||||
def test_get_category_by_id() -> None:
|
||||
"""Test retrieving category by ID."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
# Test existing category
|
||||
cat_two = manager.get_category_by_id("cat2")
|
||||
assert cat_two is not None
|
||||
assert cat_two["name"] == "Category Two"
|
||||
assert cat_two["prefix"] == "c2"
|
||||
assert "subcategories" not in cat_two # No subcategories
|
||||
|
||||
# Test non-existent category
|
||||
nonexistent = manager.get_category_by_id("nonexistent")
|
||||
assert nonexistent is None
|
||||
|
||||
|
||||
def test_get_all_subcategories() -> None:
|
||||
"""Test getting all subcategories with parent info."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
subcategories = manager.get_all_subcategories()
|
||||
|
||||
# Check we have the right number of subcategories
|
||||
assert subcategories and len(subcategories) == 3 # sub1, sub2, sub3
|
||||
|
||||
# Check subcategory structure
|
||||
sub_a = (
|
||||
next((s for s in subcategories if s["name"] == "Subcategory A"), None)
|
||||
if subcategories
|
||||
else None
|
||||
)
|
||||
assert sub_a is not None
|
||||
assert sub_a["parent"] == "Category One"
|
||||
assert sub_a["full_name"] == "Category One: Subcategory A"
|
||||
|
||||
sub_c = (
|
||||
next((s for s in subcategories if s["name"] == "Subcategory C"), None)
|
||||
if subcategories
|
||||
else None
|
||||
)
|
||||
assert sub_c is not None
|
||||
assert sub_c["parent"] == "Category Three"
|
||||
|
||||
|
||||
def test_get_subcategories_for_category() -> None:
|
||||
"""Test getting subcategories for a specific category."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
# Category with subcategories
|
||||
cat_one_subs = manager.get_subcategories_for_category("Category One")
|
||||
assert "Subcategory A" in cat_one_subs
|
||||
assert "Subcategory B" in cat_one_subs
|
||||
assert len(cat_one_subs) == 2
|
||||
|
||||
# Category without subcategories
|
||||
cat_two_subs = manager.get_subcategories_for_category("Category Two")
|
||||
assert cat_two_subs == []
|
||||
|
||||
# Non-existent category
|
||||
nonexistent_subs = manager.get_subcategories_for_category("NonExistent")
|
||||
assert nonexistent_subs == []
|
||||
|
||||
|
||||
def test_validate_category_subcategory() -> None:
|
||||
"""Test validation of category-subcategory relationships."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
# Valid combinations
|
||||
assert manager.validate_category_subcategory("Category One", "Subcategory A") is True
|
||||
assert manager.validate_category_subcategory("Category Three", "Subcategory C") is True
|
||||
|
||||
# No subcategory (always valid for existing categories)
|
||||
assert manager.validate_category_subcategory("Category Two", "") is True
|
||||
assert manager.validate_category_subcategory("Category Two", None) is True
|
||||
|
||||
# Invalid combinations
|
||||
assert manager.validate_category_subcategory("Category One", "Subcategory C") is False
|
||||
assert manager.validate_category_subcategory("Category Two", "Subcategory A") is False
|
||||
assert manager.validate_category_subcategory("NonExistent", "Something") is False
|
||||
|
||||
|
||||
def test_get_categories_for_readme() -> None:
|
||||
"""Test getting categories ordered for README generation."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
categories = manager.get_categories_for_readme()
|
||||
|
||||
# Check ordering - should be sorted by 'order' field
|
||||
assert categories[0]["id"] == "cat2" # order: 1
|
||||
assert categories[1]["id"] == "cat1" # order: 2
|
||||
assert categories[2]["id"] == "cat3" # order: 3
|
||||
|
||||
# All categories should be present
|
||||
assert len(categories) == 3
|
||||
|
||||
|
||||
def test_get_toc_config() -> None:
|
||||
"""Test getting table of contents configuration."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = create_test_categories()
|
||||
|
||||
toc_config = manager.get_toc_config()
|
||||
|
||||
# Check test TOC settings
|
||||
assert toc_config["style"] == "test"
|
||||
assert toc_config["symbol"] == "►"
|
||||
assert toc_config["subsymbol"] == "▸"
|
||||
assert toc_config["indent"] == " "
|
||||
assert toc_config["subindent"] == " "
|
||||
|
||||
|
||||
def test_singleton_behavior() -> None:
|
||||
"""Test that CategoryManager behaves as a singleton."""
|
||||
# Create new instances
|
||||
instance1 = CategoryManager()
|
||||
instance2 = CategoryManager()
|
||||
|
||||
# They should be the same object
|
||||
assert instance1 is instance2
|
||||
|
||||
|
||||
def test_loading_from_file() -> None:
|
||||
"""Test loading categories from a YAML file."""
|
||||
# Create a temporary YAML file with test data
|
||||
test_data = create_test_categories()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||
yaml.dump(test_data, f)
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
# Patch the _load_categories method to load from our temp file
|
||||
original_load = CategoryManager._load_categories
|
||||
|
||||
def mock_load(self: Any) -> None:
|
||||
with open(temp_path, encoding="utf-8") as f:
|
||||
type(self)._data = yaml.safe_load(f)
|
||||
|
||||
CategoryManager._load_categories = mock_load # type: ignore[method-assign]
|
||||
|
||||
# Create a fresh instance (reset singleton)
|
||||
CategoryManager._instance = None
|
||||
CategoryManager._data = None
|
||||
|
||||
manager = CategoryManager()
|
||||
|
||||
# Verify data was loaded correctly
|
||||
categories = manager.get_all_categories()
|
||||
assert len(categories) == 3
|
||||
assert "Category One" in categories
|
||||
|
||||
# Restore original method
|
||||
CategoryManager._load_categories = original_load # type: ignore[method-assign]
|
||||
finally:
|
||||
# Clean up
|
||||
Path(temp_path).unlink()
|
||||
|
||||
|
||||
def test_robustness_with_missing_fields() -> None:
|
||||
"""Test that the manager handles missing optional fields gracefully."""
|
||||
manager = CategoryManager()
|
||||
CategoryManager._data = {
|
||||
"categories": [
|
||||
{
|
||||
"id": "minimal",
|
||||
"name": "Minimal Category",
|
||||
"prefix": "min",
|
||||
# No icon, description, order, or subcategories
|
||||
},
|
||||
{
|
||||
"id": "partial",
|
||||
"name": "Partial Category",
|
||||
"prefix": "par",
|
||||
"icon": "🟡",
|
||||
# No description or order
|
||||
"subcategories": [],
|
||||
},
|
||||
],
|
||||
"toc": {
|
||||
"style": "minimal",
|
||||
# Other fields missing
|
||||
},
|
||||
}
|
||||
|
||||
# Should not crash when accessing categories
|
||||
categories = manager.get_all_categories()
|
||||
assert len(categories) == 2
|
||||
|
||||
# Should handle missing subcategories gracefully
|
||||
subs = manager.get_subcategories_for_category("Minimal Category")
|
||||
assert subs == []
|
||||
|
||||
# TOC config should have some defaults or handle missing fields
|
||||
toc = manager.get_toc_config()
|
||||
assert toc["style"] == "minimal"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run all tests
|
||||
test_functions = [
|
||||
test_get_all_categories,
|
||||
test_get_category_prefixes,
|
||||
test_get_category_by_name,
|
||||
test_get_category_by_id,
|
||||
test_get_all_subcategories,
|
||||
test_get_subcategories_for_category,
|
||||
test_validate_category_subcategory,
|
||||
test_get_categories_for_readme,
|
||||
test_get_toc_config,
|
||||
test_singleton_behavior,
|
||||
test_loading_from_file,
|
||||
test_robustness_with_missing_fields,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test_func in test_functions:
|
||||
try:
|
||||
test_func()
|
||||
print(f"✓ {test_func.__name__}")
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"✗ {test_func.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ {test_func.__name__}: Unexpected error: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\nTests: {passed} passed, {failed} failed")
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Tests for the informal submission detection module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.resources.detect_informal_submission import (
|
||||
Action,
|
||||
calculate_confidence,
|
||||
count_template_field_matches,
|
||||
sanitize_output,
|
||||
)
|
||||
|
||||
|
||||
class TestCountTemplateFieldMatches:
|
||||
"""Tests for template field label detection."""
|
||||
|
||||
def test_no_matches(self) -> None:
|
||||
"""No template field labels in text."""
|
||||
text = "This is a regular issue about something."
|
||||
assert count_template_field_matches(text) == 0
|
||||
|
||||
def test_single_match(self) -> None:
|
||||
"""Single template field label detected."""
|
||||
text = "Display Name: My Awesome Tool"
|
||||
assert count_template_field_matches(text) == 1
|
||||
|
||||
def test_multiple_matches(self) -> None:
|
||||
"""Multiple template field labels detected."""
|
||||
text = """
|
||||
Display Name: My Tool
|
||||
Category: Agent Skills
|
||||
Primary Link: https://github.com/example/repo
|
||||
Author Name: John Doe
|
||||
"""
|
||||
assert count_template_field_matches(text) == 4
|
||||
|
||||
def test_case_insensitive(self) -> None:
|
||||
"""Template field matching is case-insensitive."""
|
||||
text = "DISPLAY NAME: test\nPRIMARY LINK: https://example.com"
|
||||
assert count_template_field_matches(text) == 2
|
||||
|
||||
|
||||
class TestCalculateConfidence:
|
||||
"""Tests for confidence score calculation."""
|
||||
|
||||
def test_empty_input(self) -> None:
|
||||
"""Empty title and body should return no action."""
|
||||
result = calculate_confidence("", "")
|
||||
assert result.action == Action.NONE
|
||||
assert result.confidence == 0.0
|
||||
|
||||
def test_high_confidence_template_fields(self) -> None:
|
||||
"""3+ template field labels should trigger high confidence."""
|
||||
body = """
|
||||
Display Name: My Tool
|
||||
Category: Tooling
|
||||
Primary Link: https://github.com/example/repo
|
||||
Author Name: Jane
|
||||
License: MIT
|
||||
Description: A great tool for Claude Code users.
|
||||
"""
|
||||
result = calculate_confidence("New resource submission", body)
|
||||
assert result.action == Action.CLOSE
|
||||
assert result.confidence >= 0.6
|
||||
|
||||
def test_high_confidence_strong_signals(self) -> None:
|
||||
"""Clear submission language should trigger high confidence."""
|
||||
result = calculate_confidence(
|
||||
"Please add my tool to the list",
|
||||
"Check out this awesome plugin I made: https://github.com/user/repo",
|
||||
)
|
||||
assert result.action == Action.CLOSE
|
||||
assert result.confidence >= 0.6
|
||||
|
||||
def test_medium_confidence_partial_signals(self) -> None:
|
||||
"""Partial signals should trigger medium confidence (warn only)."""
|
||||
# 3 medium signals (0.15 each) = 0.45, which is WARN territory
|
||||
result = calculate_confidence(
|
||||
"Interesting project",
|
||||
"Here's a skill at github.com/user/repo under MIT license",
|
||||
)
|
||||
assert result.action == Action.WARN
|
||||
assert 0.4 <= result.confidence < 0.6
|
||||
|
||||
def test_low_confidence_bug_report(self) -> None:
|
||||
"""Bug reports should not trigger any action."""
|
||||
result = calculate_confidence(
|
||||
"Bug: validation script crashes",
|
||||
"When I try to run the validation, I get an error. The problem is with the parser.",
|
||||
)
|
||||
assert result.action == Action.NONE
|
||||
assert result.confidence < 0.4
|
||||
|
||||
def test_low_confidence_question(self) -> None:
|
||||
"""Pure questions should not trigger any action."""
|
||||
result = calculate_confidence(
|
||||
"How does the validation work?",
|
||||
"What is the process for reviewing submissions? I want to understand.",
|
||||
)
|
||||
assert result.action == Action.NONE
|
||||
assert result.confidence < 0.4
|
||||
|
||||
def test_negative_signals_reduce_score(self) -> None:
|
||||
"""Negative signals should reduce the confidence score."""
|
||||
# This has a URL (medium signal) but also bug language (negative)
|
||||
result = calculate_confidence(
|
||||
"My tool is broken",
|
||||
"The plugin at github.com/user/repo is not working and has an error.",
|
||||
)
|
||||
# The negative signals should counteract the positive ones
|
||||
assert result.confidence < 0.6
|
||||
|
||||
def test_feature_request_no_action(self) -> None:
|
||||
"""Feature requests should not trigger action."""
|
||||
result = calculate_confidence(
|
||||
"Feature request: add dark mode",
|
||||
"It would be nice if the README had a dark mode toggle.",
|
||||
)
|
||||
assert result.action == Action.NONE
|
||||
|
||||
def test_combined_signals(self) -> None:
|
||||
"""Multiple strong signals should result in high confidence."""
|
||||
result = calculate_confidence(
|
||||
"Recommend new plugin",
|
||||
"""
|
||||
I created this awesome plugin that should be added to the list.
|
||||
Check out github.com/user/cool-plugin
|
||||
It has MIT license.
|
||||
""",
|
||||
)
|
||||
assert result.action == Action.CLOSE
|
||||
assert len(result.matched_signals) >= 3
|
||||
|
||||
def test_score_clamped_to_one(self) -> None:
|
||||
"""Score should never exceed 1.0."""
|
||||
# Lots of positive signals
|
||||
body = """
|
||||
Display Name: tool
|
||||
Category: Skills
|
||||
Primary Link: https://github.com/a/b
|
||||
Author Name: x
|
||||
License: MIT
|
||||
Description: test
|
||||
I recommend this submission. Please add this new plugin.
|
||||
I built this tool and created it myself.
|
||||
Check out this awesome skill for agent workflows.
|
||||
"""
|
||||
result = calculate_confidence("Please add my new tool submission", body)
|
||||
assert result.confidence <= 1.0
|
||||
|
||||
def test_score_clamped_to_zero(self) -> None:
|
||||
"""Score should never go below 0.0."""
|
||||
# Lots of negative signals, no positive
|
||||
result = calculate_confidence(
|
||||
"Bug: How do I fix this error?",
|
||||
"The problem is not working. It's broken and failed. What is wrong?",
|
||||
)
|
||||
assert result.confidence >= 0.0
|
||||
|
||||
|
||||
class TestMatchedSignals:
|
||||
"""Tests for matched signal reporting."""
|
||||
|
||||
def test_strong_signals_labeled(self) -> None:
|
||||
"""Strong signals should be labeled in matched_signals."""
|
||||
result = calculate_confidence("Please add my tool", "I recommend this submission")
|
||||
strong_signals = [s for s in result.matched_signals if s.startswith("strong:")]
|
||||
assert len(strong_signals) > 0
|
||||
|
||||
def test_medium_signals_labeled(self) -> None:
|
||||
"""Medium signals should be labeled in matched_signals."""
|
||||
result = calculate_confidence("Test", "https://github.com/user/repo")
|
||||
medium_signals = [s for s in result.matched_signals if s.startswith("medium:")]
|
||||
assert len(medium_signals) > 0
|
||||
|
||||
def test_negative_signals_labeled(self) -> None:
|
||||
"""Negative signals should be labeled in matched_signals."""
|
||||
result = calculate_confidence("Bug report", "This has an error and is broken")
|
||||
negative_signals = [s for s in result.matched_signals if s.startswith("negative:")]
|
||||
assert len(negative_signals) > 0
|
||||
|
||||
def test_template_fields_labeled(self) -> None:
|
||||
"""Template field matches should be labeled in matched_signals."""
|
||||
result = calculate_confidence("Test", "Display Name: X\nCategory: Y")
|
||||
template_signals = [s for s in result.matched_signals if s.startswith("template-fields:")]
|
||||
assert len(template_signals) == 1
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Edge case tests."""
|
||||
|
||||
def test_url_with_bug_language(self) -> None:
|
||||
"""URL mention with bug language should not trigger action."""
|
||||
result = calculate_confidence(
|
||||
"Issue with repo",
|
||||
"The plugin at github.com/user/repo crashes with an error",
|
||||
)
|
||||
# Bug language should neutralize the URL signal
|
||||
assert result.action in (Action.NONE, Action.WARN)
|
||||
|
||||
def test_partial_template_fields(self) -> None:
|
||||
"""1-2 template fields should give medium confidence at most."""
|
||||
result = calculate_confidence("", "Display Name: test\nDescription: something")
|
||||
# 2 fields = 0.4 score, which is exactly at medium threshold
|
||||
assert result.action in (Action.NONE, Action.WARN)
|
||||
|
||||
def test_license_mention_alone(self) -> None:
|
||||
"""License mention alone should not trigger high confidence."""
|
||||
result = calculate_confidence("Question about MIT", "Is this MIT licensed?")
|
||||
assert result.action == Action.NONE
|
||||
|
||||
def test_category_mention_in_question(self) -> None:
|
||||
"""Category mention in a question context should not trigger."""
|
||||
result = calculate_confidence(
|
||||
"How do hooks work?",
|
||||
"What is the difference between agent skills and hooks?",
|
||||
)
|
||||
assert result.action == Action.NONE
|
||||
|
||||
|
||||
class TestSanitizeOutput:
|
||||
"""Tests for output sanitization (security)."""
|
||||
|
||||
def test_removes_newlines(self) -> None:
|
||||
"""Newlines should be replaced to prevent output injection."""
|
||||
result = sanitize_output("line1\nline2\nline3")
|
||||
assert "\n" not in result
|
||||
assert result == "line1 line2 line3"
|
||||
|
||||
def test_removes_carriage_returns(self) -> None:
|
||||
"""Carriage returns should be replaced."""
|
||||
result = sanitize_output("line1\r\nline2")
|
||||
assert "\r" not in result
|
||||
assert "\n" not in result
|
||||
|
||||
def test_removes_null_bytes(self) -> None:
|
||||
"""Null bytes should be removed."""
|
||||
result = sanitize_output("before\0after")
|
||||
assert "\0" not in result
|
||||
assert result == "beforeafter"
|
||||
|
||||
def test_preserves_normal_text(self) -> None:
|
||||
"""Normal text should pass through unchanged."""
|
||||
normal = "This is a normal string with spaces and punctuation!"
|
||||
assert sanitize_output(normal) == normal
|
||||
|
||||
def test_handles_empty_string(self) -> None:
|
||||
"""Empty string should return empty string."""
|
||||
assert sanitize_output("") == ""
|
||||
|
||||
def test_injection_attempt_via_newline(self) -> None:
|
||||
"""Simulated injection attempt should be neutralized."""
|
||||
# An attacker might try to inject a fake output variable
|
||||
malicious = "legitimate\nmalicious_var=evil_value"
|
||||
result = sanitize_output(malicious)
|
||||
assert "malicious_var=evil_value" in result # Content preserved
|
||||
assert "\n" not in result # But newline removed
|
||||
assert result == "legitimate malicious_var=evil_value"
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for repo ticker data fetching utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import scripts.ticker.fetch_repo_ticker_data as fetch_repo_ticker_data # noqa: E402
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
"""Minimal response stub for requests.get."""
|
||||
|
||||
def __init__(self, payload: dict, status_code: int = 200) -> None:
|
||||
self._payload = payload
|
||||
self.status_code = status_code
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
if self.status_code >= 400:
|
||||
raise requests.exceptions.HTTPError(f"status {self.status_code}")
|
||||
|
||||
def json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
|
||||
def test_load_previous_data_missing_file(tmp_path: Path) -> None:
|
||||
assert fetch_repo_ticker_data.load_previous_data(tmp_path / "missing.csv") == {}
|
||||
|
||||
|
||||
def test_load_previous_data_reads_csv(tmp_path: Path) -> None:
|
||||
path = tmp_path / "previous.csv"
|
||||
with path.open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=["full_name", "stars", "watchers", "forks"])
|
||||
writer.writeheader()
|
||||
writer.writerow({"full_name": "owner/repo", "stars": "5", "watchers": "2", "forks": "1"})
|
||||
|
||||
data = fetch_repo_ticker_data.load_previous_data(path)
|
||||
assert data == {"owner/repo": {"stars": 5, "watchers": 2, "forks": 1}}
|
||||
|
||||
|
||||
def test_calculate_deltas_with_previous() -> None:
|
||||
repos = [{"full_name": "owner/repo", "stars": 10, "watchers": 5, "forks": 3}]
|
||||
previous = {"owner/repo": {"stars": 7, "watchers": 2, "forks": 1}}
|
||||
result = fetch_repo_ticker_data.calculate_deltas(repos, previous)
|
||||
assert result[0]["stars_delta"] == 3
|
||||
assert result[0]["watchers_delta"] == 3
|
||||
assert result[0]["forks_delta"] == 2
|
||||
|
||||
|
||||
def test_calculate_deltas_new_repo_with_prior_snapshot() -> None:
|
||||
repos = [{"full_name": "owner/new", "stars": 4, "watchers": 3, "forks": 2}]
|
||||
previous = {"owner/old": {"stars": 1, "watchers": 1, "forks": 1}}
|
||||
result = fetch_repo_ticker_data.calculate_deltas(repos, previous)
|
||||
assert result[0]["stars_delta"] == 4
|
||||
assert result[0]["watchers_delta"] == 3
|
||||
assert result[0]["forks_delta"] == 2
|
||||
|
||||
|
||||
def test_calculate_deltas_no_previous_baseline() -> None:
|
||||
repos = [{"full_name": "owner/new", "stars": 4, "watchers": 3, "forks": 2}]
|
||||
result = fetch_repo_ticker_data.calculate_deltas(repos, {})
|
||||
assert result[0]["stars_delta"] == 0
|
||||
assert result[0]["watchers_delta"] == 0
|
||||
assert result[0]["forks_delta"] == 0
|
||||
|
||||
|
||||
def test_save_to_csv_writes_output(tmp_path: Path) -> None:
|
||||
output_path = tmp_path / "data" / "repo-ticker.csv"
|
||||
repos = [
|
||||
{
|
||||
"full_name": "owner/repo",
|
||||
"stars": 10,
|
||||
"watchers": 5,
|
||||
"forks": 2,
|
||||
"stars_delta": 1,
|
||||
"watchers_delta": 0,
|
||||
"forks_delta": 2,
|
||||
"url": "https://github.com/owner/repo",
|
||||
}
|
||||
]
|
||||
|
||||
fetch_repo_ticker_data.save_to_csv(repos, output_path)
|
||||
|
||||
with output_path.open(encoding="utf-8") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
assert rows[0]["full_name"] == "owner/repo"
|
||||
assert rows[0]["stars_delta"] == "1"
|
||||
|
||||
|
||||
def test_fetch_repos_maps_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
token = "token"
|
||||
payload = {
|
||||
"items": [
|
||||
{
|
||||
"full_name": "owner/repo",
|
||||
"stargazers_count": 10,
|
||||
"watchers_count": 5,
|
||||
"forks_count": 2,
|
||||
"html_url": "https://github.com/owner/repo",
|
||||
}
|
||||
]
|
||||
}
|
||||
captured_url: str | None = None
|
||||
captured_params: dict[str, str | int] | None = None
|
||||
captured_headers: dict[str, str] | None = None
|
||||
captured_timeout: int | None = None
|
||||
|
||||
def fake_get(url, params=None, headers=None, timeout=None):
|
||||
nonlocal captured_url, captured_params, captured_headers, captured_timeout
|
||||
captured_url = url
|
||||
captured_params = params
|
||||
captured_headers = headers
|
||||
captured_timeout = timeout
|
||||
return DummyResponse(payload)
|
||||
|
||||
monkeypatch.setattr(fetch_repo_ticker_data.requests, "get", fake_get)
|
||||
|
||||
repos = fetch_repo_ticker_data.fetch_repos(token)
|
||||
assert repos == [
|
||||
{
|
||||
"full_name": "owner/repo",
|
||||
"stars": 10,
|
||||
"watchers": 5,
|
||||
"forks": 2,
|
||||
"url": "https://github.com/owner/repo",
|
||||
}
|
||||
]
|
||||
assert captured_url == "https://api.github.com/search/repositories"
|
||||
assert captured_headers is not None
|
||||
assert captured_params is not None
|
||||
assert captured_headers["Authorization"] == f"Bearer {token}"
|
||||
assert captured_params["per_page"] == 100
|
||||
assert captured_timeout == 30
|
||||
|
||||
|
||||
def test_fetch_repos_request_error_exits(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_get(*_args, **_kwargs):
|
||||
raise requests.exceptions.RequestException("boom")
|
||||
|
||||
monkeypatch.setattr(fetch_repo_ticker_data.requests, "get", fake_get)
|
||||
with pytest.raises(SystemExit):
|
||||
fetch_repo_ticker_data.fetch_repos("token")
|
||||
|
||||
|
||||
def test_main_missing_token_exits(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
|
||||
with pytest.raises(SystemExit):
|
||||
fetch_repo_ticker_data.main()
|
||||
@@ -0,0 +1,584 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for flat list README generation functionality."""
|
||||
|
||||
import csv
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add repo root to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.readme.generators.flat import ( # noqa: E402
|
||||
FLAT_CATEGORIES,
|
||||
FLAT_SORT_TYPES,
|
||||
ParameterizedFlatListGenerator,
|
||||
)
|
||||
from scripts.readme.helpers.readme_assets import generate_flat_badges # noqa: E402
|
||||
from scripts.readme.helpers.readme_paths import resolve_asset_tokens # noqa: E402
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlatListEnv:
|
||||
"""Filesystem paths for flat list generator tests."""
|
||||
|
||||
root: Path
|
||||
template_dir: Path
|
||||
assets_dir: Path
|
||||
csv_path: Path
|
||||
|
||||
|
||||
DEFAULT_ROWS = [
|
||||
{
|
||||
"ID": "test-1",
|
||||
"Display Name": "Test Resource",
|
||||
"Category": "Tooling",
|
||||
"Sub-Category": "General",
|
||||
"Primary Link": "https://github.com/test/repo",
|
||||
"Author Name": "Test Author",
|
||||
"Author Link": "https://github.com/testauthor",
|
||||
"Description": "A test resource",
|
||||
"Active": "TRUE",
|
||||
"Last Modified": "2025-01-01",
|
||||
"Repo Created": "2024-06-01",
|
||||
},
|
||||
{
|
||||
"ID": "test-2",
|
||||
"Display Name": "Another Resource",
|
||||
"Category": "Hooks",
|
||||
"Sub-Category": "General",
|
||||
"Primary Link": "https://github.com/test/hooks",
|
||||
"Author Name": "Hook Author",
|
||||
"Author Link": "https://github.com/hookauthor",
|
||||
"Description": "A hooks resource",
|
||||
"Active": "TRUE",
|
||||
"Last Modified": "2025-01-15",
|
||||
"Repo Created": "2024-12-01",
|
||||
},
|
||||
]
|
||||
|
||||
MINIMAL_ROWS = [
|
||||
{
|
||||
"ID": "test-1",
|
||||
"Display Name": "Test",
|
||||
"Category": "Tooling",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://example.com/author",
|
||||
"Description": "Test",
|
||||
"Active": "TRUE",
|
||||
"Last Modified": "2025-01-01",
|
||||
"Repo Created": "2024-01-01",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def write_csv(path: Path, rows: list[dict[str, str]]) -> None:
|
||||
"""Write CSV rows to disk."""
|
||||
if not rows:
|
||||
path.write_text("", encoding="utf-8")
|
||||
return
|
||||
|
||||
with path.open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def create_env(tmp_path: Path, rows: list[dict[str, str]]) -> FlatListEnv:
|
||||
"""Create a temp environment with CSV, templates, and assets."""
|
||||
template_dir = tmp_path / "templates"
|
||||
assets_dir = tmp_path / "assets"
|
||||
template_dir.mkdir()
|
||||
assets_dir.mkdir()
|
||||
csv_path = tmp_path / "test.csv"
|
||||
write_csv(csv_path, rows)
|
||||
|
||||
return FlatListEnv(
|
||||
root=tmp_path,
|
||||
template_dir=template_dir,
|
||||
assets_dir=assets_dir,
|
||||
csv_path=csv_path,
|
||||
)
|
||||
|
||||
|
||||
def make_generator(env: FlatListEnv, category_slug: str = "all", sort_type: str = "az"):
|
||||
"""Create a generator instance using the provided env."""
|
||||
return ParameterizedFlatListGenerator(
|
||||
str(env.csv_path),
|
||||
str(env.template_dir),
|
||||
str(env.assets_dir),
|
||||
str(env.root),
|
||||
category_slug=category_slug,
|
||||
sort_type=sort_type,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flat_list_env(tmp_path: Path) -> FlatListEnv:
|
||||
"""Environment with default CSV rows."""
|
||||
return create_env(tmp_path, DEFAULT_ROWS)
|
||||
|
||||
|
||||
class TestFlatCategories:
|
||||
"""Test cases for FLAT_CATEGORIES configuration."""
|
||||
|
||||
def test_all_category_exists(self) -> None:
|
||||
"""Test that 'all' category exists and has None as csv_value."""
|
||||
assert "all" in FLAT_CATEGORIES
|
||||
csv_value, display, color = FLAT_CATEGORIES["all"]
|
||||
assert csv_value is None
|
||||
assert display == "All"
|
||||
|
||||
def test_all_categories_have_required_fields(self) -> None:
|
||||
"""Test all categories have (csv_value, display_name, color) tuple."""
|
||||
for _, value in FLAT_CATEGORIES.items():
|
||||
assert isinstance(value, tuple)
|
||||
assert len(value) == 3
|
||||
_, display, color = value
|
||||
assert isinstance(display, str)
|
||||
assert color.startswith("#")
|
||||
|
||||
def test_expected_categories_exist(self) -> None:
|
||||
"""Test that expected categories are defined."""
|
||||
expected = [
|
||||
"all",
|
||||
"tooling",
|
||||
"commands",
|
||||
"claude-md",
|
||||
"workflows",
|
||||
"hooks",
|
||||
"skills",
|
||||
"styles",
|
||||
"statusline",
|
||||
"docs",
|
||||
"clients",
|
||||
]
|
||||
for cat in expected:
|
||||
assert cat in FLAT_CATEGORIES, f"Missing category: {cat}"
|
||||
|
||||
def test_category_count(self) -> None:
|
||||
"""Test we have 11 categories."""
|
||||
assert len(FLAT_CATEGORIES) == 11
|
||||
|
||||
|
||||
class TestFlatSortTypes:
|
||||
"""Test cases for FLAT_SORT_TYPES configuration."""
|
||||
|
||||
def test_all_sort_types_exist(self) -> None:
|
||||
"""Test all expected sort types are defined."""
|
||||
expected = ["az", "updated", "created", "releases"]
|
||||
for sort_type in expected:
|
||||
assert sort_type in FLAT_SORT_TYPES
|
||||
|
||||
def test_sort_types_have_required_fields(self) -> None:
|
||||
"""Test all sort types have (display, color, description) tuple."""
|
||||
for _, value in FLAT_SORT_TYPES.items():
|
||||
assert isinstance(value, tuple)
|
||||
assert len(value) == 3
|
||||
display, color, description = value
|
||||
assert isinstance(display, str)
|
||||
assert color.startswith("#")
|
||||
assert isinstance(description, str)
|
||||
|
||||
def test_sort_type_count(self) -> None:
|
||||
"""Test we have 4 sort types."""
|
||||
assert len(FLAT_SORT_TYPES) == 4
|
||||
|
||||
|
||||
class TestParameterizedFlatListGenerator:
|
||||
"""Test cases for ParameterizedFlatListGenerator class."""
|
||||
|
||||
def test_output_filename_format(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test output filename follows expected pattern."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="az")
|
||||
assert generator.output_filename == "README_ALTERNATIVES/README_FLAT_ALL_AZ.md"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cat_slug, sort_type, expected",
|
||||
[
|
||||
("tooling", "updated", "README_ALTERNATIVES/README_FLAT_TOOLING_UPDATED.md"),
|
||||
("hooks", "releases", "README_ALTERNATIVES/README_FLAT_HOOKS_RELEASES.md"),
|
||||
("claude-md", "created", "README_ALTERNATIVES/README_FLAT_CLAUDE-MD_CREATED.md"),
|
||||
],
|
||||
)
|
||||
def test_output_filename_with_different_params(
|
||||
self, flat_list_env: FlatListEnv, cat_slug: str, sort_type: str, expected: str
|
||||
) -> None:
|
||||
"""Test output filename with various category/sort combinations."""
|
||||
generator = make_generator(flat_list_env, category_slug=cat_slug, sort_type=sort_type)
|
||||
assert generator.output_filename == expected
|
||||
|
||||
def test_get_filtered_resources_all(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test filtering with 'all' category returns all resources."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="az")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
assert len(resources) == 2
|
||||
|
||||
def test_get_filtered_resources_specific_category(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test filtering with specific category."""
|
||||
generator = make_generator(flat_list_env, category_slug="tooling", sort_type="az")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0]["Display Name"] == "Test Resource"
|
||||
|
||||
def test_get_filtered_resources_hooks_category(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test filtering with hooks category."""
|
||||
generator = make_generator(flat_list_env, category_slug="hooks", sort_type="az")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0]["Display Name"] == "Another Resource"
|
||||
|
||||
def test_sort_resources_alphabetical(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test alphabetical sorting."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="az")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
sorted_resources = generator.sort_resources(resources)
|
||||
|
||||
# "Another Resource" should come before "Test Resource"
|
||||
assert sorted_resources[0]["Display Name"] == "Another Resource"
|
||||
assert sorted_resources[1]["Display Name"] == "Test Resource"
|
||||
|
||||
def test_sort_resources_by_updated(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test sorting by last modified date."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="updated")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
sorted_resources = generator.sort_resources(resources)
|
||||
|
||||
# "Another Resource" (2025-01-15) should come before "Test Resource" (2025-01-01)
|
||||
assert sorted_resources[0]["Display Name"] == "Another Resource"
|
||||
assert sorted_resources[1]["Display Name"] == "Test Resource"
|
||||
|
||||
def test_sort_resources_by_created(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test sorting by repo creation date."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="created")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
sorted_resources = generator.sort_resources(resources)
|
||||
|
||||
# "Another Resource" (2024-12-01) should come before "Test Resource" (2024-06-01)
|
||||
assert sorted_resources[0]["Display Name"] == "Another Resource"
|
||||
assert sorted_resources[1]["Display Name"] == "Test Resource"
|
||||
|
||||
def test_generate_sort_navigation(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test sort navigation badge generation."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="az")
|
||||
nav = generator.generate_sort_navigation()
|
||||
resolved = resolve_asset_tokens(
|
||||
nav,
|
||||
flat_list_env.root / "README_ALTERNATIVES" / "README_FLAT_ALL_AZ.md",
|
||||
flat_list_env.root,
|
||||
)
|
||||
|
||||
# Check for all sort options
|
||||
assert "README_FLAT_ALL_AZ" in nav
|
||||
assert "README_FLAT_ALL_UPDATED" in nav
|
||||
assert "README_FLAT_ALL_CREATED" in nav
|
||||
assert "README_FLAT_ALL_RELEASES" in nav
|
||||
|
||||
# Check current selection has border
|
||||
assert 'style="border: 3px solid #6366f1' in nav # az color
|
||||
|
||||
# Check asset paths use ../assets/ (one level up)
|
||||
assert 'src="../assets/badge-sort-az.svg"' in resolved
|
||||
|
||||
def test_generate_category_navigation(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test category navigation badge generation."""
|
||||
generator = make_generator(flat_list_env, category_slug="hooks", sort_type="az")
|
||||
nav = generator.generate_category_navigation()
|
||||
resolved = resolve_asset_tokens(
|
||||
nav,
|
||||
flat_list_env.root / "README_ALTERNATIVES" / "README_FLAT_HOOKS_AZ.md",
|
||||
flat_list_env.root,
|
||||
)
|
||||
|
||||
# Check for category links (should maintain current sort type)
|
||||
assert "README_FLAT_ALL_AZ" in nav
|
||||
assert "README_FLAT_TOOLING_AZ" in nav
|
||||
assert "README_FLAT_HOOKS_AZ" in nav
|
||||
|
||||
# Check hooks has border (current selection)
|
||||
assert 'style="border: 2px solid #f97316' in nav # hooks color
|
||||
|
||||
# Check asset paths use ../assets/ (one level up)
|
||||
assert 'src="../assets/badge-cat-hooks.svg"' in resolved
|
||||
|
||||
def test_generate_resources_table_standard(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test resources table generation for non-releases view."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="az")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
table = generator.generate_resources_table()
|
||||
|
||||
# Check HTML table structure
|
||||
assert "<table>" in table
|
||||
assert "<thead>" in table
|
||||
assert "<th>Resource</th>" in table
|
||||
assert "<th>Category</th>" in table
|
||||
assert "<th>Sub-Category</th>" in table
|
||||
assert "<th>Description</th>" in table
|
||||
|
||||
# Check stacked format (now HTML)
|
||||
assert "<b>Another Resource</b>" in table
|
||||
assert "<br>by" in table
|
||||
|
||||
# Check full description (no truncation)
|
||||
assert "A hooks resource" in table
|
||||
|
||||
# Check shields.io badges are present for GitHub resources
|
||||
assert "img.shields.io/github/stars" in table
|
||||
assert "?style=flat" in table
|
||||
|
||||
def test_generate_resources_table_empty_category(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test resources table for empty category."""
|
||||
generator = make_generator(flat_list_env, category_slug="clients", sort_type="az")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
table = generator.generate_resources_table()
|
||||
|
||||
assert "No resources found in this category" in table
|
||||
|
||||
def test_default_template_has_correct_paths(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test default template uses style selector placeholder."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="az")
|
||||
template = generator._get_default_template()
|
||||
|
||||
# Template should use {{STYLE_SELECTOR}} placeholder for dynamic path generation
|
||||
assert "{{STYLE_SELECTOR}}" in template
|
||||
|
||||
def test_releases_disclaimer_in_template(self, flat_list_env: FlatListEnv) -> None:
|
||||
"""Test releases view includes disclaimer."""
|
||||
generator = make_generator(flat_list_env, category_slug="all", sort_type="releases")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
|
||||
template = generator._get_default_template()
|
||||
assert "{{RELEASES_DISCLAIMER}}" in template
|
||||
|
||||
|
||||
class TestGenerateFlatBadges:
|
||||
"""Test cases for generate_flat_badges function."""
|
||||
|
||||
def test_creates_sort_badges(self, tmp_path: Path) -> None:
|
||||
"""Test that sort badges are created."""
|
||||
generate_flat_badges(str(tmp_path), FLAT_SORT_TYPES, FLAT_CATEGORIES)
|
||||
|
||||
for slug in FLAT_SORT_TYPES:
|
||||
badge_path = tmp_path / f"badge-sort-{slug}.svg"
|
||||
assert badge_path.exists(), f"Missing badge: {badge_path}"
|
||||
|
||||
def test_creates_category_badges(self, tmp_path: Path) -> None:
|
||||
"""Test that category badges are created."""
|
||||
generate_flat_badges(str(tmp_path), FLAT_SORT_TYPES, FLAT_CATEGORIES)
|
||||
|
||||
for slug in FLAT_CATEGORIES:
|
||||
badge_path = tmp_path / f"badge-cat-{slug}.svg"
|
||||
assert badge_path.exists(), f"Missing badge: {badge_path}"
|
||||
|
||||
def test_badge_is_valid_svg(self, tmp_path: Path) -> None:
|
||||
"""Test that generated badges are valid SVG."""
|
||||
generate_flat_badges(str(tmp_path), FLAT_SORT_TYPES, FLAT_CATEGORIES)
|
||||
|
||||
badge_path = tmp_path / "badge-sort-az.svg"
|
||||
content = badge_path.read_text(encoding="utf-8")
|
||||
|
||||
assert "<svg" in content
|
||||
assert "</svg>" in content
|
||||
assert "xmlns=" in content
|
||||
|
||||
def test_sort_badge_contains_display_name(self, tmp_path: Path) -> None:
|
||||
"""Test sort badges contain correct display names."""
|
||||
generate_flat_badges(str(tmp_path), FLAT_SORT_TYPES, FLAT_CATEGORIES)
|
||||
|
||||
badge_path = tmp_path / "badge-sort-az.svg"
|
||||
content = badge_path.read_text(encoding="utf-8")
|
||||
|
||||
display_name = FLAT_SORT_TYPES["az"][0]
|
||||
assert display_name in content
|
||||
|
||||
def test_category_badge_contains_display_name(self, tmp_path: Path) -> None:
|
||||
"""Test category badges contain correct display names."""
|
||||
generate_flat_badges(str(tmp_path), FLAT_SORT_TYPES, FLAT_CATEGORIES)
|
||||
|
||||
badge_path = tmp_path / "badge-cat-hooks.svg"
|
||||
content = badge_path.read_text(encoding="utf-8")
|
||||
|
||||
display_name = FLAT_CATEGORIES["hooks"][1]
|
||||
assert display_name in content
|
||||
|
||||
|
||||
class TestReleasesSort:
|
||||
"""Test cases for releases sorting functionality."""
|
||||
|
||||
def test_releases_filter_recent(self, tmp_path: Path) -> None:
|
||||
"""Test that releases sort only includes recent releases."""
|
||||
now = datetime.now()
|
||||
recent = (now - timedelta(days=10)).strftime("%Y-%m-%d:%H-%M-%S")
|
||||
old = (now - timedelta(days=60)).strftime("%Y-%m-%d:%H-%M-%S")
|
||||
|
||||
rows = [
|
||||
{
|
||||
"ID": "recent-1",
|
||||
"Display Name": "Recent Release",
|
||||
"Category": "Tooling",
|
||||
"Primary Link": "https://github.com/test/recent",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://github.com/author",
|
||||
"Description": "Has recent release",
|
||||
"Active": "TRUE",
|
||||
"Latest Release": recent,
|
||||
"Release Version": "v1.0.0",
|
||||
"Release Source": "github-releases",
|
||||
},
|
||||
{
|
||||
"ID": "old-1",
|
||||
"Display Name": "Old Release",
|
||||
"Category": "Tooling",
|
||||
"Primary Link": "https://github.com/test/old",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://github.com/author",
|
||||
"Description": "Has old release",
|
||||
"Active": "TRUE",
|
||||
"Latest Release": old,
|
||||
"Release Version": "v0.5.0",
|
||||
"Release Source": "github-releases",
|
||||
},
|
||||
]
|
||||
env = create_env(tmp_path, rows)
|
||||
|
||||
generator = make_generator(env, category_slug="all", sort_type="releases")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
sorted_resources = generator.sort_resources(resources)
|
||||
|
||||
# Only recent release should be included
|
||||
assert len(sorted_resources) == 1
|
||||
assert sorted_resources[0]["Display Name"] == "Recent Release"
|
||||
|
||||
def test_releases_sort_order(self, tmp_path: Path) -> None:
|
||||
"""Test that releases are sorted by date (most recent first)."""
|
||||
now = datetime.now()
|
||||
day5 = (now - timedelta(days=5)).strftime("%Y-%m-%d:%H-%M-%S")
|
||||
day10 = (now - timedelta(days=10)).strftime("%Y-%m-%d:%H-%M-%S")
|
||||
day15 = (now - timedelta(days=15)).strftime("%Y-%m-%d:%H-%M-%S")
|
||||
|
||||
rows = [
|
||||
{
|
||||
"ID": "mid",
|
||||
"Display Name": "Middle Release",
|
||||
"Category": "Tooling",
|
||||
"Primary Link": "https://github.com/test/mid",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://github.com/author",
|
||||
"Description": "10 days ago",
|
||||
"Active": "TRUE",
|
||||
"Latest Release": day10,
|
||||
"Release Version": "v1.0.0",
|
||||
"Release Source": "github-releases",
|
||||
},
|
||||
{
|
||||
"ID": "newest",
|
||||
"Display Name": "Newest Release",
|
||||
"Category": "Tooling",
|
||||
"Primary Link": "https://github.com/test/new",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://github.com/author",
|
||||
"Description": "5 days ago",
|
||||
"Active": "TRUE",
|
||||
"Latest Release": day5,
|
||||
"Release Version": "v2.0.0",
|
||||
"Release Source": "github-releases",
|
||||
},
|
||||
{
|
||||
"ID": "oldest",
|
||||
"Display Name": "Oldest Release",
|
||||
"Category": "Tooling",
|
||||
"Primary Link": "https://github.com/test/old",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://github.com/author",
|
||||
"Description": "15 days ago",
|
||||
"Active": "TRUE",
|
||||
"Latest Release": day15,
|
||||
"Release Version": "v0.5.0",
|
||||
"Release Source": "github-releases",
|
||||
},
|
||||
]
|
||||
env = create_env(tmp_path, rows)
|
||||
|
||||
generator = make_generator(env, category_slug="all", sort_type="releases")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
resources = generator.get_filtered_resources()
|
||||
sorted_resources = generator.sort_resources(resources)
|
||||
|
||||
assert len(sorted_resources) == 3
|
||||
assert sorted_resources[0]["Display Name"] == "Newest Release"
|
||||
assert sorted_resources[1]["Display Name"] == "Middle Release"
|
||||
assert sorted_resources[2]["Display Name"] == "Oldest Release"
|
||||
|
||||
def test_releases_table_format(self, tmp_path: Path) -> None:
|
||||
"""Test releases table has correct columns."""
|
||||
now = datetime.now()
|
||||
recent = (now - timedelta(days=5)).strftime("%Y-%m-%d:%H-%M-%S")
|
||||
|
||||
rows = [
|
||||
{
|
||||
"ID": "test-1",
|
||||
"Display Name": "Test Package",
|
||||
"Category": "Tooling",
|
||||
"Primary Link": "https://github.com/test/pkg",
|
||||
"Author Name": "Test Author",
|
||||
"Author Link": "https://github.com/testauthor",
|
||||
"Description": "A test package with release",
|
||||
"Active": "TRUE",
|
||||
"Latest Release": recent,
|
||||
"Release Version": "v1.2.3",
|
||||
"Release Source": "npm",
|
||||
},
|
||||
]
|
||||
env = create_env(tmp_path, rows)
|
||||
|
||||
generator = make_generator(env, category_slug="all", sort_type="releases")
|
||||
generator.csv_data = generator.load_csv_data()
|
||||
table = generator.generate_resources_table()
|
||||
|
||||
# Check HTML table header columns
|
||||
assert "<table>" in table
|
||||
assert "<th>Resource</th>" in table
|
||||
assert "<th>Version</th>" in table
|
||||
assert "<th>Source</th>" in table
|
||||
assert "<th>Release Date</th>" in table
|
||||
assert "<th>Description</th>" in table
|
||||
|
||||
# Check content
|
||||
assert "v1.2.3" in table
|
||||
assert "npm" in table
|
||||
assert "Test Package" in table
|
||||
|
||||
# Check shields.io badges with colspan="5"
|
||||
assert 'colspan="5"' in table
|
||||
assert "img.shields.io/github/stars" in table
|
||||
|
||||
|
||||
class TestCombinationGeneration:
|
||||
"""Test that all category × sort combinations work correctly."""
|
||||
|
||||
@pytest.mark.parametrize("cat_slug", FLAT_CATEGORIES)
|
||||
@pytest.mark.parametrize("sort_type", FLAT_SORT_TYPES)
|
||||
def test_all_combinations_instantiate(
|
||||
self, tmp_path: Path, cat_slug: str, sort_type: str
|
||||
) -> None:
|
||||
"""Test all 44 combinations can be instantiated."""
|
||||
env = create_env(tmp_path, MINIMAL_ROWS)
|
||||
generator = make_generator(env, category_slug=cat_slug, sort_type=sort_type)
|
||||
assert generator is not None
|
||||
|
||||
def test_total_combinations(self) -> None:
|
||||
"""Test that we expect 44 total combinations (11 × 4)."""
|
||||
expected = len(FLAT_CATEGORIES) * len(FLAT_SORT_TYPES)
|
||||
assert expected == 44
|
||||
880
.agent/knowledge/awesome_claude/tests/test_generate_readme.py
Normal file
880
.agent/knowledge/awesome_claude/tests/test_generate_readme.py
Normal file
@@ -0,0 +1,880 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for README generation functions."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
# Add repo root to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from scripts.readme.helpers.readme_utils import ( # noqa: E402
|
||||
get_anchor_suffix_for_icon,
|
||||
parse_resource_date,
|
||||
)
|
||||
from scripts.readme.markup.minimal import ( # noqa: E402
|
||||
format_resource_entry,
|
||||
generate_section_content,
|
||||
generate_toc,
|
||||
generate_weekly_section,
|
||||
)
|
||||
from scripts.readme.markup.shared import load_announcements # noqa: E402
|
||||
|
||||
|
||||
def write_yaml(path: os.PathLike[str], data: Any) -> None:
|
||||
"""Write YAML data to a file."""
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
|
||||
class TestParseResourceDate:
|
||||
"""Test cases for the parse_resource_date function."""
|
||||
|
||||
def test_parse_date_only_format(self) -> None:
|
||||
"""Test parsing YYYY-MM-DD format."""
|
||||
result = parse_resource_date("2025-08-07")
|
||||
expected = datetime(2025, 8, 7)
|
||||
assert result == expected
|
||||
|
||||
def test_parse_date_with_timestamp_format(self) -> None:
|
||||
"""Test parsing YYYY-MM-DD:HH-MM-SS format."""
|
||||
result = parse_resource_date("2025-08-07:18-26-57")
|
||||
expected = datetime(2025, 8, 7, 18, 26, 57)
|
||||
assert result == expected
|
||||
|
||||
def test_parse_with_whitespace(self) -> None:
|
||||
"""Test parsing with leading/trailing whitespace."""
|
||||
result = parse_resource_date(" 2025-08-07 ")
|
||||
expected = datetime(2025, 8, 7)
|
||||
assert result == expected
|
||||
|
||||
def test_parse_empty_string(self) -> None:
|
||||
"""Test parsing empty string returns None."""
|
||||
assert parse_resource_date("") is None
|
||||
|
||||
def test_parse_none(self) -> None:
|
||||
"""Test parsing None returns None."""
|
||||
assert parse_resource_date(None) is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_date",
|
||||
[
|
||||
"2025/08/07", # Wrong separator
|
||||
"07-08-2025", # Wrong order
|
||||
"2025-13-01", # Invalid month
|
||||
"2025-08-32", # Invalid day
|
||||
"not-a-date", # Complete nonsense
|
||||
"2025-08-07 18:26:57", # Space instead of colon
|
||||
],
|
||||
)
|
||||
def test_parse_invalid_format(self, invalid_date: str) -> None:
|
||||
"""Test parsing invalid date format returns None."""
|
||||
assert parse_resource_date(invalid_date) is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"date_string, expected",
|
||||
[
|
||||
("2025-08-05:11-48-39", datetime(2025, 8, 5, 11, 48, 39)),
|
||||
("2025-07-29:18-37-05", datetime(2025, 7, 29, 18, 37, 5)),
|
||||
("2025-08-07:00-00-00", datetime(2025, 8, 7, 0, 0, 0)),
|
||||
("2025-12-31:23-59-59", datetime(2025, 12, 31, 23, 59, 59)),
|
||||
],
|
||||
)
|
||||
def test_parse_various_timestamps(self, date_string: str, expected: datetime) -> None:
|
||||
"""Test parsing various valid timestamp formats."""
|
||||
assert parse_resource_date(date_string) == expected
|
||||
|
||||
def test_date_comparison(self) -> None:
|
||||
"""Test that parsed dates can be compared correctly."""
|
||||
date1 = parse_resource_date("2025-08-07")
|
||||
date2 = parse_resource_date("2025-08-05")
|
||||
date3 = parse_resource_date("2025-08-07:18-26-57")
|
||||
|
||||
assert date1 is not None and date2 is not None and date3 is not None
|
||||
assert date1 > date2
|
||||
assert date3 > date1 # Same date but with time
|
||||
assert not date2 > date1
|
||||
|
||||
|
||||
class TestGetAnchorSuffix:
|
||||
"""Test cases for the get_anchor_suffix_for_icon function."""
|
||||
|
||||
@pytest.mark.parametrize("icon", ["", None])
|
||||
def test_no_icon(self, icon: str | None) -> None:
|
||||
"""Test empty icon returns empty string."""
|
||||
assert get_anchor_suffix_for_icon(icon) == ""
|
||||
|
||||
@pytest.mark.parametrize("icon", ["🎯", "💡", "🔧"])
|
||||
def test_simple_emoji(self, icon: str) -> None:
|
||||
"""Test simple emoji returns dash."""
|
||||
assert get_anchor_suffix_for_icon(icon) == "-"
|
||||
|
||||
def test_emoji_with_variation_selector(self) -> None:
|
||||
"""Test emoji with VS-16 returns URL-encoded suffix."""
|
||||
# Classical Building emoji with VS-16
|
||||
assert get_anchor_suffix_for_icon("🏛️") == "-%EF%B8%8F"
|
||||
|
||||
|
||||
class TestGenerateTOC:
|
||||
"""Test cases for the generate_toc function."""
|
||||
|
||||
def test_empty_categories(self) -> None:
|
||||
"""Test TOC generation with no categories."""
|
||||
result = generate_toc([], [])
|
||||
|
||||
# Check for main structure (open by default)
|
||||
assert "## Contents [🔝](#awesome-claude-code)" in result
|
||||
assert "<details open>" in result
|
||||
assert "<summary>Table of Contents</summary>" in result
|
||||
assert "</details>" in result
|
||||
|
||||
def test_simple_categories(self) -> None:
|
||||
"""Test TOC generation with simple categories (no subcategories)."""
|
||||
categories = [
|
||||
{"name": "Getting Started", "icon": "🚀"},
|
||||
{"name": "Resources", "icon": "📚"},
|
||||
{"name": "Tools", "icon": "🔧"},
|
||||
]
|
||||
result = generate_toc(categories, [])
|
||||
|
||||
# Check for main structure
|
||||
assert "<summary>Table of Contents</summary>" in result
|
||||
|
||||
# Check for simple links (CLASSIC style adds extra dash for 🔝 back-to-top)
|
||||
assert "- [Getting Started](#getting-started--)" in result
|
||||
assert "- [Resources](#resources--)" in result
|
||||
assert "- [Tools](#tools--)" in result
|
||||
|
||||
def test_categories_with_subcategories(self) -> None:
|
||||
"""Test TOC generation with categories containing subcategories."""
|
||||
categories: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "Configuration",
|
||||
"icon": "⚙️",
|
||||
"subcategories": [
|
||||
{"name": "Basic Setup"},
|
||||
{"name": "Advanced Options"},
|
||||
],
|
||||
},
|
||||
{"name": "Simple Category"}, # No subcategories
|
||||
]
|
||||
csv_data = [
|
||||
{"Category": "Configuration", "Sub-Category": "Basic Setup"},
|
||||
{"Category": "Configuration", "Sub-Category": "Advanced Options"},
|
||||
]
|
||||
result = generate_toc(categories, csv_data)
|
||||
|
||||
# Check for collapsible category with subcategories (open by default)
|
||||
assert "- <details open>" in result
|
||||
# CLASSIC style: icon suffix + extra dash for 🔝 back-to-top
|
||||
assert ' <summary><a href="#configuration-%EF%B8%8F-">Configuration</a>' in result
|
||||
|
||||
# Check for subcategories (CLASSIC adds trailing dash for 🔝)
|
||||
assert " - [Basic Setup](#basic-setup-)" in result
|
||||
assert " - [Advanced Options](#advanced-options-)" in result
|
||||
|
||||
# Check for simple category (CLASSIC adds extra dash for 🔝)
|
||||
assert "- [Simple Category](#simple-category-)" in result
|
||||
|
||||
def test_special_characters_in_names(self) -> None:
|
||||
"""Test TOC generation with special characters in category names."""
|
||||
categories = [
|
||||
{"name": "Tips & Tricks"},
|
||||
{"name": "CI/CD Tools"},
|
||||
{"name": "Node.js Resources"},
|
||||
]
|
||||
result = generate_toc(categories, [])
|
||||
|
||||
# Check that special characters are properly handled in anchors
|
||||
# CLASSIC style adds extra dash for 🔝 back-to-top
|
||||
assert "[Tips & Tricks](#tips--tricks-)" in result
|
||||
assert "[CI/CD Tools](#cicd-tools-)" in result
|
||||
assert "[Node.js Resources](#nodejs-resources-)" in result
|
||||
|
||||
def test_mixed_categories(self) -> None:
|
||||
"""Test TOC with a mix of simple and nested categories."""
|
||||
categories: list[dict[str, Any]] = [
|
||||
{"name": "Overview"},
|
||||
{
|
||||
"name": "Documentation",
|
||||
"icon": "📖",
|
||||
"subcategories": [
|
||||
{"name": "API Reference"},
|
||||
{"name": "Tutorials"},
|
||||
],
|
||||
},
|
||||
{"name": "Community", "icon": "👥"},
|
||||
{
|
||||
"name": "Development",
|
||||
"subcategories": [{"name": "Contributing"}],
|
||||
},
|
||||
]
|
||||
csv_data = [
|
||||
{"Category": "Documentation", "Sub-Category": "API Reference"},
|
||||
{"Category": "Documentation", "Sub-Category": "Tutorials"},
|
||||
{"Category": "Development", "Sub-Category": "Contributing"},
|
||||
]
|
||||
result = generate_toc(categories, csv_data)
|
||||
|
||||
lines = result.split("\n")
|
||||
|
||||
# Should have main details wrapper (open by default)
|
||||
assert lines[0] == "## Contents [🔝](#awesome-claude-code)"
|
||||
assert lines[2] == "<details open>"
|
||||
assert lines[3] == "<summary>Table of Contents</summary>"
|
||||
|
||||
# Check for simple categories (CLASSIC adds extra dash for 🔝)
|
||||
assert "- [Overview](#overview-)" in result
|
||||
assert "- [Community](#community--)" in result
|
||||
|
||||
# Check for nested categories (CLASSIC: icon dash + 🔝 dash)
|
||||
assert ' <summary><a href="#documentation--">Documentation</a>' in result
|
||||
assert " - [API Reference](#api-reference-)" in result
|
||||
assert " - [Tutorials](#tutorials-)" in result
|
||||
|
||||
# Count details blocks (main + 2 categories with subcategories) - all open by default
|
||||
assert result.count("<details open>") == 3
|
||||
assert result.count("</details>") == 3
|
||||
|
||||
|
||||
class TestLoadAnnouncements:
|
||||
"""Test cases for the load_announcements function."""
|
||||
|
||||
def test_empty_announcements(self, tmp_path) -> None:
|
||||
"""Test loading empty announcements."""
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
yaml_path.write_text("", encoding="utf-8")
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
assert result == ""
|
||||
|
||||
def test_simple_string_announcement(self, tmp_path) -> None:
|
||||
"""Test announcements with simple string items."""
|
||||
announcements_data = [
|
||||
{
|
||||
"date": "2025-09-12",
|
||||
"title": "Test Announcements",
|
||||
"items": ["First announcement", "Second announcement"],
|
||||
}
|
||||
]
|
||||
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
write_yaml(yaml_path, announcements_data)
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
|
||||
# Check for header and main structure
|
||||
assert "### Announcements [🔝](#awesome-claude-code)" in result
|
||||
assert "<details open>" in result
|
||||
assert "<summary>View Announcements</summary>" in result
|
||||
|
||||
# Check for date group
|
||||
assert "- <details open>" in result
|
||||
assert "<summary>2025-09-12 - Test Announcements</summary>" in result
|
||||
|
||||
# Check for items
|
||||
assert " - First announcement" in result
|
||||
assert " - Second announcement" in result
|
||||
|
||||
def test_collapsible_announcement_items(self, tmp_path) -> None:
|
||||
"""Test announcements with collapsible summary/text items."""
|
||||
announcements_data = [
|
||||
{
|
||||
"date": "2025-09-12",
|
||||
"title": "Feature Updates",
|
||||
"items": [
|
||||
{
|
||||
"summary": "New feature added",
|
||||
"text": "This is a detailed description of the new feature.",
|
||||
},
|
||||
{
|
||||
"summary": "Bug fix",
|
||||
"text": "Fixed a critical bug in the system.",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
write_yaml(yaml_path, announcements_data)
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
|
||||
# Check for header
|
||||
assert "### Announcements [🔝](#awesome-claude-code)" in result
|
||||
|
||||
# Check for nested collapsible items
|
||||
assert " - <details open>" in result
|
||||
assert " <summary>New feature added</summary>" in result
|
||||
assert " - This is a detailed description of the new feature." in result
|
||||
assert " <summary>Bug fix</summary>" in result
|
||||
assert " - Fixed a critical bug in the system." in result
|
||||
|
||||
def test_multi_line_text_in_announcements(self, tmp_path) -> None:
|
||||
"""Test announcements with multi-line text content."""
|
||||
announcements_data = [
|
||||
{
|
||||
"date": "2025-09-15",
|
||||
"title": "Important Notice",
|
||||
"items": [
|
||||
{
|
||||
"summary": "Multi-line announcement",
|
||||
"text": (
|
||||
"Line 1 of the announcement.\n\nLine 2 with a gap.\n\nLine 3 final."
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
write_yaml(yaml_path, announcements_data)
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
|
||||
# Check for header
|
||||
assert "### Announcements [🔝](#awesome-claude-code)" in result
|
||||
|
||||
# Check that multi-line text is properly formatted
|
||||
assert " - Line 1 of the announcement." in result
|
||||
assert " Line 2 with a gap." in result
|
||||
assert " Line 3 final." in result
|
||||
|
||||
def test_mixed_announcement_types(self, tmp_path) -> None:
|
||||
"""Test announcements with mixed item types."""
|
||||
announcements_data = [
|
||||
{
|
||||
"date": "2025-09-20",
|
||||
"items": [ # No title
|
||||
"Simple string item",
|
||||
{"summary": "Collapsible item", "text": "Detailed content here"},
|
||||
{"summary": "Summary only item"}, # No text
|
||||
{"text": "Text only item"}, # No summary
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
write_yaml(yaml_path, announcements_data)
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
|
||||
# Check for header
|
||||
assert "### Announcements [🔝](#awesome-claude-code)" in result
|
||||
|
||||
# Check for date without title
|
||||
assert "<summary>2025-09-20</summary>" in result
|
||||
|
||||
# Check for various item types
|
||||
assert " - Simple string item" in result
|
||||
assert " <summary>Collapsible item</summary>" in result
|
||||
assert " - Detailed content here" in result
|
||||
assert " - Summary only item" in result
|
||||
assert " - Text only item" in result
|
||||
|
||||
def test_multiple_date_groups(self, tmp_path) -> None:
|
||||
"""Test announcements with multiple date groups."""
|
||||
announcements_data = [
|
||||
{
|
||||
"date": "2025-09-10",
|
||||
"title": "Week 1",
|
||||
"items": ["Announcement 1"],
|
||||
},
|
||||
{
|
||||
"date": "2025-09-17",
|
||||
"title": "Week 2",
|
||||
"items": ["Announcement 2"],
|
||||
},
|
||||
]
|
||||
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
write_yaml(yaml_path, announcements_data)
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
|
||||
# Check for header
|
||||
assert "### Announcements [🔝](#awesome-claude-code)" in result
|
||||
|
||||
# Check for both date groups
|
||||
assert "<summary>2025-09-10 - Week 1</summary>" in result
|
||||
assert "<summary>2025-09-17 - Week 2</summary>" in result
|
||||
|
||||
# Verify proper nesting structure
|
||||
assert result.count("- <details open>") == 2 # Two date groups
|
||||
assert result.count("</details>") == 3 # Main + 2 date groups
|
||||
|
||||
def test_markdown_in_announcements(self, tmp_path) -> None:
|
||||
"""Test that markdown formatting is preserved in announcements."""
|
||||
announcements_data = [
|
||||
{
|
||||
"date": "2025-09-12",
|
||||
"title": "Markdown Test",
|
||||
"items": [
|
||||
{
|
||||
"summary": "Test with markdown",
|
||||
"text": "This has **bold** text and [a link](https://example.com).",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
yaml_path = tmp_path / "announcements.yaml"
|
||||
write_yaml(yaml_path, announcements_data)
|
||||
|
||||
result = load_announcements(str(tmp_path))
|
||||
|
||||
# Check for header
|
||||
assert "### Announcements [🔝](#awesome-claude-code)" in result
|
||||
|
||||
# Check that markdown is preserved
|
||||
assert "**bold**" in result
|
||||
assert "[a link](https://example.com)" in result
|
||||
|
||||
def test_nonexistent_directory(self, tmp_path) -> None:
|
||||
"""Test loading from a directory with no announcement files."""
|
||||
empty_dir = tmp_path / "empty"
|
||||
empty_dir.mkdir()
|
||||
|
||||
result = load_announcements(str(empty_dir))
|
||||
assert result == ""
|
||||
|
||||
|
||||
class TestGenerateSectionContent:
|
||||
"""Test cases for the generate_section_content function."""
|
||||
|
||||
def test_simple_category_with_resources(self) -> None:
|
||||
"""Test generating a simple category section with resources."""
|
||||
category = {
|
||||
"name": "Tools",
|
||||
"icon": "🔧",
|
||||
"subcategories": [{"name": "General"}],
|
||||
}
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Tools",
|
||||
"Sub-Category": "General",
|
||||
"Display Name": "Tool 1",
|
||||
"Primary Link": "https://example.com/tool1",
|
||||
"Author Name": "Author 1",
|
||||
"Author Link": "",
|
||||
"Description": "A useful tool",
|
||||
"License": "MIT",
|
||||
}
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# Header with back-to-top link
|
||||
assert "## Tools 🔧 [🔝](#awesome-claude-code)" in result
|
||||
assert "<details open>" in result
|
||||
|
||||
# Check for resource content
|
||||
assert "[`Tool 1`](https://example.com/tool1)" in result
|
||||
assert "A useful tool" in result
|
||||
|
||||
def test_category_with_description(self) -> None:
|
||||
"""Test generating a category with a description."""
|
||||
category = {
|
||||
"name": "Resources",
|
||||
"icon": "📚",
|
||||
"description": "Helpful resources for developers",
|
||||
}
|
||||
csv_data: list[dict[str, Any]] = []
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
assert "## Resources 📚 [🔝](#awesome-claude-code)" in result
|
||||
assert "> Helpful resources for developers" in result
|
||||
|
||||
def test_category_with_subcategories(self) -> None:
|
||||
"""Test generating a category with subcategories."""
|
||||
category = {
|
||||
"name": "Documentation",
|
||||
"icon": "📖",
|
||||
"subcategories": [
|
||||
{"name": "Tutorials"},
|
||||
{"name": "API Reference"},
|
||||
],
|
||||
}
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Documentation",
|
||||
"Sub-Category": "Tutorials",
|
||||
"Display Name": "Getting Started",
|
||||
"Primary Link": "https://example.com/tutorial",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"Description": "",
|
||||
"License": "",
|
||||
},
|
||||
{
|
||||
"Category": "Documentation",
|
||||
"Sub-Category": "API Reference",
|
||||
"Display Name": "API Docs",
|
||||
"Primary Link": "https://example.com/api",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"Description": "",
|
||||
"License": "",
|
||||
},
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# Categories WITH subcategories should NOT be wrapped in details at the main level
|
||||
assert "## Documentation 📖 [🔝](#awesome-claude-code)" in result
|
||||
assert "<summary><h2>Documentation 📖" not in result
|
||||
|
||||
# Check for subcategory details wrappers
|
||||
assert result.count("<details open>") == 2 # Only 2 subcategories
|
||||
assert (
|
||||
'<summary><h3>Tutorials <a href="#awesome-claude-code">🔝</a></h3></summary>' in result
|
||||
)
|
||||
assert (
|
||||
'<summary><h3>API Reference <a href="#awesome-claude-code">🔝</a></h3></summary>'
|
||||
in result
|
||||
)
|
||||
|
||||
# Check for resources in subcategories
|
||||
assert "[`Getting Started`](https://example.com/tutorial)" in result
|
||||
assert "[`API Docs`](https://example.com/api)" in result
|
||||
|
||||
# Check closing tags
|
||||
assert result.count("</details>") == 2
|
||||
|
||||
def test_category_with_main_and_sub_resources(self) -> None:
|
||||
"""Test a category with resources at both main and sub levels."""
|
||||
category = {
|
||||
"name": "Mixed",
|
||||
"subcategories": [{"name": "Subcategory"}],
|
||||
}
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Mixed",
|
||||
"Sub-Category": "",
|
||||
"Display Name": "Main Resource",
|
||||
"Primary Link": "https://example.com/main",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"Description": "",
|
||||
"License": "",
|
||||
},
|
||||
{
|
||||
"Category": "Mixed",
|
||||
"Sub-Category": "Subcategory",
|
||||
"Display Name": "Sub Resource",
|
||||
"Primary Link": "https://example.com/sub",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"Description": "",
|
||||
"License": "",
|
||||
},
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# Main-level resources are not rendered in minimal mode
|
||||
assert "Main Resource" not in result
|
||||
assert "Sub Resource" in result
|
||||
|
||||
assert "## Mixed [🔝](#awesome-claude-code)" in result
|
||||
assert result.count("<details open>") == 1
|
||||
|
||||
def test_category_without_icon(self) -> None:
|
||||
"""Test generating a category without an icon."""
|
||||
category = {"name": "Plain Category"}
|
||||
csv_data: list[dict[str, Any]] = []
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
assert "## Plain Category [🔝](#awesome-claude-code)" in result
|
||||
|
||||
def test_empty_subcategory_not_rendered(self) -> None:
|
||||
"""Test that subcategories without resources are not rendered."""
|
||||
category = {
|
||||
"name": "Test",
|
||||
"subcategories": [
|
||||
{"name": "Empty Sub"},
|
||||
{"name": "Has Resources"},
|
||||
],
|
||||
}
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Test",
|
||||
"Sub-Category": "Has Resources",
|
||||
"Display Name": "Resource",
|
||||
"Primary Link": "https://example.com",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"Description": "",
|
||||
"License": "",
|
||||
}
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# Empty subcategory should not be present
|
||||
assert "Empty Sub" not in result
|
||||
|
||||
# Subcategory with resources should be present
|
||||
assert "Has Resources" in result
|
||||
|
||||
# Categories WITH subcategories have regular headers
|
||||
assert "## Test [🔝](#awesome-claude-code)" in result
|
||||
# Should only have 1 details block (the subcategory with resources)
|
||||
assert result.count("<details open>") == 1
|
||||
|
||||
|
||||
class TestBackToTopButtons:
|
||||
"""Test cases for back-to-top button functionality."""
|
||||
|
||||
def test_weekly_section_has_back_to_top(self) -> None:
|
||||
"""Test that the weekly section header has a back-to-top button."""
|
||||
csv_data: list[dict[str, str]] = []
|
||||
result = generate_weekly_section(csv_data)
|
||||
|
||||
# Check that the header contains the back-to-top link
|
||||
assert "## Latest Additions ✨ [🔝](#awesome-claude-code)" in result
|
||||
|
||||
def test_category_without_subcategories_has_html_anchor(self) -> None:
|
||||
"""Test categories without subcategories render markdown back-to-top links."""
|
||||
category = {
|
||||
"name": "Test Category",
|
||||
"icon": "🧪",
|
||||
"description": "Test description",
|
||||
}
|
||||
csv_data: list[dict[str, str]] = []
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
assert "## Test Category 🧪 [🔝](#awesome-claude-code)" in result
|
||||
assert "<summary><h2>" not in result
|
||||
|
||||
def test_category_without_icon_has_back_to_top(self) -> None:
|
||||
"""Test categories without icons still get back-to-top buttons."""
|
||||
category = {"name": "No Icon Category", "description": "Test description"}
|
||||
csv_data: list[dict[str, str]] = []
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
assert "## No Icon Category [🔝](#awesome-claude-code)" in result
|
||||
|
||||
def test_category_with_subcategories_has_markdown_link(self) -> None:
|
||||
"""Test categories with subcategories have markdown link (not in summary)."""
|
||||
category = {
|
||||
"name": "Main Category",
|
||||
"icon": "📁",
|
||||
"subcategories": [{"name": "Sub One"}, {"name": "Sub Two"}],
|
||||
}
|
||||
csv_data: list[dict[str, str]] = []
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# Main category should have markdown link (it's a regular header, not in summary)
|
||||
assert "## Main Category 📁 [🔝](#awesome-claude-code)" in result
|
||||
# Should NOT have HTML anchor for main category
|
||||
assert "## Main Category 📁 <a href=" not in result
|
||||
|
||||
def test_subcategory_has_html_anchor(self) -> None:
|
||||
"""Test subcategories have HTML anchor in their summary tags."""
|
||||
category = {
|
||||
"name": "Main",
|
||||
"icon": "📁",
|
||||
"subcategories": [{"name": "Subcategory Test"}],
|
||||
}
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Main",
|
||||
"Sub-Category": "Subcategory Test",
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "https://example.com",
|
||||
"Active": "TRUE",
|
||||
}
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# Subcategory should use HTML anchor inside summary
|
||||
assert (
|
||||
'<summary><h3>Subcategory Test <a href="#awesome-claude-code">🔝</a></h3></summary>'
|
||||
in result
|
||||
)
|
||||
# Should NOT have markdown link in subcategory summary
|
||||
assert "[🔝](#awesome-claude-code)</h3></summary>" not in result
|
||||
|
||||
def test_multiple_subcategories_all_have_anchors(self) -> None:
|
||||
"""Test that all subcategories get back-to-top anchors."""
|
||||
category: dict[str, Any] = {
|
||||
"name": "Parent",
|
||||
"subcategories": [
|
||||
{"name": "First Sub"},
|
||||
{"name": "Second Sub"},
|
||||
{"name": "Third Sub"},
|
||||
],
|
||||
}
|
||||
subcategories = category.get("subcategories", [])
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Parent",
|
||||
"Sub-Category": sub["name"],
|
||||
"Display Name": f"Resource for {sub['name']}",
|
||||
"Primary Link": "https://example.com",
|
||||
"Active": "TRUE",
|
||||
}
|
||||
for sub in subcategories
|
||||
if isinstance(sub, dict)
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
# All subcategories should have HTML anchors
|
||||
assert (
|
||||
'<summary><h3>First Sub <a href="#awesome-claude-code">🔝</a></h3></summary>' in result
|
||||
)
|
||||
assert (
|
||||
'<summary><h3>Second Sub <a href="#awesome-claude-code">🔝</a></h3></summary>' in result
|
||||
)
|
||||
assert (
|
||||
'<summary><h3>Third Sub <a href="#awesome-claude-code">🔝</a></h3></summary>' in result
|
||||
)
|
||||
|
||||
# Count that we have exactly 3 back-to-top anchors in summaries
|
||||
anchor_count = result.count('<a href="#awesome-claude-code">🔝</a></h3></summary>')
|
||||
assert anchor_count == 3
|
||||
|
||||
def test_back_to_top_preserves_existing_structure(self) -> None:
|
||||
"""Test that adding back-to-top doesn't break existing structure."""
|
||||
category = {
|
||||
"name": "Complete Test",
|
||||
"icon": "✅",
|
||||
"description": "A complete category",
|
||||
"subcategories": [{"name": "Complete Sub"}],
|
||||
}
|
||||
csv_data = [
|
||||
{
|
||||
"Category": "Complete Test",
|
||||
"Sub-Category": "",
|
||||
"Display Name": "Main Resource",
|
||||
"Primary Link": "https://example.com/main",
|
||||
"Active": "TRUE",
|
||||
"Description": "Main description",
|
||||
},
|
||||
{
|
||||
"Category": "Complete Test",
|
||||
"Sub-Category": "Complete Sub",
|
||||
"Display Name": "Sub Resource",
|
||||
"Primary Link": "https://example.com/sub",
|
||||
"Active": "TRUE",
|
||||
"Description": "Sub description",
|
||||
},
|
||||
]
|
||||
|
||||
result = generate_section_content(category, csv_data)
|
||||
|
||||
assert "## Complete Test ✅ [🔝](#awesome-claude-code)" in result
|
||||
assert "> A complete category" in result
|
||||
assert "<details open>" in result
|
||||
assert (
|
||||
'<summary><h3>Complete Sub <a href="#awesome-claude-code">🔝</a></h3></summary>'
|
||||
in result
|
||||
)
|
||||
assert "[`Sub Resource`](https://example.com/sub)" in result
|
||||
assert "Sub description" in result
|
||||
assert "</details>" in result
|
||||
|
||||
|
||||
class TestFormatResourceEntryGitHubStats:
|
||||
"""Test GitHub stats disclosure functionality in format_resource_entry."""
|
||||
|
||||
def test_github_resource_with_stats(self) -> None:
|
||||
"""Test that GitHub resources get stats disclosure."""
|
||||
row = {
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "https://github.com/owner/repo",
|
||||
"Description": "Test description",
|
||||
"Author Name": "Test Author",
|
||||
"Author Link": "https://github.com/testauthor",
|
||||
"License": "MIT",
|
||||
}
|
||||
|
||||
result = format_resource_entry(row)
|
||||
|
||||
# Check for disclosure element
|
||||
assert "<details>" in result
|
||||
assert "📊 GitHub Stats" in result
|
||||
assert (
|
||||
"https://github-readme-stats-fork-orpin.vercel.app"
|
||||
"/api/pin/?repo=repo&username=owner&all_stats=true&stats_only=true" in result
|
||||
)
|
||||
|
||||
def test_non_github_resource_no_stats(self) -> None:
|
||||
"""Test that non-GitHub resources don't get stats disclosure."""
|
||||
row = {
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "https://example.com/resource",
|
||||
"Description": "Test description",
|
||||
"Author Name": "Test Author",
|
||||
"Author Link": "",
|
||||
"License": "",
|
||||
}
|
||||
|
||||
result = format_resource_entry(row)
|
||||
|
||||
# Should not have disclosure element
|
||||
assert "<details>" not in result
|
||||
assert "GitHub Stats" not in result
|
||||
|
||||
def test_github_blob_url_with_stats(self) -> None:
|
||||
"""Test GitHub blob URLs also get stats."""
|
||||
row = {
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "https://github.com/owner/repo/blob/main/.claude/commands",
|
||||
"Description": "Test description",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"License": "",
|
||||
}
|
||||
|
||||
result = format_resource_entry(row)
|
||||
|
||||
# Check for disclosure element with correct owner/repo
|
||||
assert "<details>" in result
|
||||
assert "repo=repo&username=owner" in result
|
||||
|
||||
def test_github_tree_url_with_stats(self) -> None:
|
||||
"""Test GitHub tree URLs also get stats."""
|
||||
row = {
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "https://github.com/owner/repo/tree/main/.claude/commands",
|
||||
"Description": "Test description",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"License": "",
|
||||
}
|
||||
|
||||
result = format_resource_entry(row)
|
||||
|
||||
# Check for disclosure element with correct owner/repo
|
||||
assert "<details>" in result
|
||||
assert "repo=repo&username=owner" in result
|
||||
|
||||
def test_empty_primary_link_no_stats(self) -> None:
|
||||
"""Test that resources without primary link don't get stats."""
|
||||
row = {
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "",
|
||||
"Description": "Test description",
|
||||
"Author Name": "",
|
||||
"Author Link": "",
|
||||
"License": "",
|
||||
}
|
||||
|
||||
result = format_resource_entry(row)
|
||||
|
||||
# Should not have disclosure element
|
||||
assert "<details>" not in result
|
||||
assert "GitHub Stats" not in result
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for ticker SVG generation functions."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add repo root to path so we can import the module
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.ticker.generate_ticker_svg import truncate_repo_name # noqa: E402
|
||||
|
||||
|
||||
def test_truncate_repo_name_short():
|
||||
"""Test that short names are not truncated."""
|
||||
assert truncate_repo_name("short-name") == "short-name"
|
||||
assert truncate_repo_name("a") == "a"
|
||||
assert truncate_repo_name("") == ""
|
||||
|
||||
|
||||
def test_truncate_repo_name_exactly_20():
|
||||
"""Test that names exactly 20 characters are not truncated."""
|
||||
name_20_chars = "12345678901234567890"
|
||||
assert len(name_20_chars) == 20
|
||||
assert truncate_repo_name(name_20_chars) == name_20_chars
|
||||
|
||||
|
||||
def test_truncate_repo_name_long():
|
||||
"""Test that long names are truncated with ellipsis."""
|
||||
long_name = "very-long-repository-name-that-exceeds-twenty-chars"
|
||||
result = truncate_repo_name(long_name)
|
||||
assert result == "very-long-repository..."
|
||||
assert len(result) == 23 # 20 chars + "..."
|
||||
|
||||
|
||||
def test_truncate_repo_name_custom_length():
|
||||
"""Test truncation with custom max length."""
|
||||
name = "this-is-a-long-name"
|
||||
result = truncate_repo_name(name, max_length=10)
|
||||
assert result == "this-is-a-..."
|
||||
assert len(result) == 13 # 10 chars + "..."
|
||||
|
||||
|
||||
def test_truncate_repo_name_preserves_beginning():
|
||||
"""Test that truncation preserves the beginning of the name."""
|
||||
name = "claude-code-infrastructure-showcase"
|
||||
result = truncate_repo_name(name)
|
||||
assert result.startswith("claude-code-infrastr")
|
||||
assert result.endswith("...")
|
||||
|
||||
|
||||
def test_truncate_repo_name_edge_cases():
|
||||
"""Test edge cases for truncation."""
|
||||
# Name exactly one character longer than max
|
||||
name_21_chars = "123456789012345678901"
|
||||
assert len(name_21_chars) == 21
|
||||
result = truncate_repo_name(name_21_chars)
|
||||
assert result == "12345678901234567890..."
|
||||
|
||||
# Very long name
|
||||
very_long = "a" * 100
|
||||
result = truncate_repo_name(very_long)
|
||||
assert len(result) == 23 # 20 chars + "..."
|
||||
assert result == "a" * 20 + "..."
|
||||
169
.agent/knowledge/awesome_claude/tests/test_git_utils.py
Normal file
169
.agent/knowledge/awesome_claude/tests/test_git_utils.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for GitUtils helper methods."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.utils.git_utils import GitUtils # noqa: E402
|
||||
|
||||
|
||||
def test_check_command_exists_true(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(returncode=0)
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().check_command_exists("git") is True
|
||||
|
||||
|
||||
def test_check_command_exists_false_on_missing(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
raise FileNotFoundError("missing")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().check_command_exists("nope") is False
|
||||
|
||||
|
||||
def test_run_command_failure_logs_error(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(returncode=1, stderr="bad", stdout="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
utils = GitUtils(logger=logging.getLogger("git-utils-test"))
|
||||
with caplog.at_level(logging.ERROR):
|
||||
assert utils.run_command(["git", "status"], error_msg="oops") is False
|
||||
assert any("oops: bad" in message for message in caplog.messages)
|
||||
|
||||
|
||||
def test_is_gh_authenticated(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(returncode=0, stdout="user\n")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().is_gh_authenticated() is True
|
||||
|
||||
|
||||
def test_is_gh_authenticated_false_on_empty_stdout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(returncode=0, stdout="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().is_gh_authenticated() is False
|
||||
|
||||
|
||||
def test_get_github_username_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(stdout="octocat\n")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().get_github_username() == "octocat"
|
||||
|
||||
|
||||
def test_get_github_username_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
raise subprocess.CalledProcessError(1, "gh api")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().get_github_username() is None
|
||||
|
||||
|
||||
def test_get_git_config_returns_value(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(stdout="value\n")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().get_git_config("user.name") == "value"
|
||||
|
||||
|
||||
def test_get_remote_type_variants(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
utils = GitUtils()
|
||||
monkeypatch.setattr(utils, "get_remote_url", lambda *_: "git@github.com:owner/repo.git")
|
||||
assert utils.get_remote_type() == "ssh"
|
||||
|
||||
monkeypatch.setattr(utils, "get_remote_url", lambda *_: "https://github.com/owner/repo")
|
||||
assert utils.get_remote_type() == "https"
|
||||
|
||||
monkeypatch.setattr(utils, "get_remote_url", lambda *_: "file:///tmp/repo")
|
||||
assert utils.get_remote_type() is None
|
||||
|
||||
|
||||
def test_is_working_directory_clean(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(stdout="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().is_working_directory_clean() is True
|
||||
|
||||
|
||||
def test_is_working_directory_dirty(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(stdout=" M file.txt\n")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().is_working_directory_clean() is False
|
||||
|
||||
|
||||
def test_get_uncommitted_files(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(stdout=" M file.txt\n")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().get_uncommitted_files() == "M file.txt"
|
||||
|
||||
|
||||
def test_stage_file_success(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(returncode=0, stderr="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().stage_file(tmp_path / "file.txt") is True
|
||||
|
||||
|
||||
def test_stage_file_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
def fake_run(*_args, **_kwargs):
|
||||
return SimpleNamespace(returncode=1, stderr="bad")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().stage_file(tmp_path / "file.txt") is False
|
||||
|
||||
|
||||
def test_check_file_modified(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
def fake_run(cmd, *args, **kwargs):
|
||||
if cmd[:3] == ["git", "diff", "--name-only"]:
|
||||
return SimpleNamespace(stdout="file.txt\n")
|
||||
if cmd[:4] == ["git", "diff", "--cached", "--name-only"]:
|
||||
return SimpleNamespace(stdout="")
|
||||
return SimpleNamespace(stdout="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().check_file_modified(tmp_path / "file.txt") is True
|
||||
|
||||
|
||||
def test_check_file_modified_staged(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
def fake_run(cmd, *args, **kwargs):
|
||||
if cmd[:3] == ["git", "diff", "--name-only"]:
|
||||
return SimpleNamespace(stdout="")
|
||||
if cmd[:4] == ["git", "diff", "--cached", "--name-only"]:
|
||||
return SimpleNamespace(stdout="file.txt\n")
|
||||
return SimpleNamespace(stdout="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().check_file_modified(tmp_path / "file.txt") is True
|
||||
|
||||
|
||||
def test_check_file_modified_clean(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
def fake_run(cmd, *args, **kwargs):
|
||||
return SimpleNamespace(stdout="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert GitUtils().check_file_modified(tmp_path / "file.txt") is False
|
||||
102
.agent/knowledge/awesome_claude/tests/test_github_utils.py
Normal file
102
.agent/knowledge/awesome_claude/tests/test_github_utils.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for GitHub utility parsing helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.utils.github_utils import parse_github_resource_url, parse_github_url # noqa: E402
|
||||
|
||||
|
||||
def test_parse_github_url_blob_with_slash_branch() -> None:
|
||||
url = "https://github.com/owner/repo/blob/feature/with/slash/README.md"
|
||||
api_url, is_github, owner, repo = parse_github_url(url)
|
||||
assert is_github is True
|
||||
assert owner == "owner"
|
||||
assert repo == "repo"
|
||||
assert (
|
||||
api_url
|
||||
== "https://api.github.com/repos/owner/repo/contents/README.md?ref=feature%2Fwith%2Fslash"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_github_url_tree_docs_path() -> None:
|
||||
url = "https://github.com/owner/repo/tree/main/docs"
|
||||
api_url, is_github, owner, repo = parse_github_url(url)
|
||||
assert is_github is True
|
||||
assert owner == "owner"
|
||||
assert repo == "repo"
|
||||
assert api_url == "https://api.github.com/repos/owner/repo/contents/docs?ref=main"
|
||||
|
||||
|
||||
def test_parse_github_url_repo_root() -> None:
|
||||
url = "https://github.com/owner/repo.git"
|
||||
api_url, is_github, owner, repo = parse_github_url(url)
|
||||
assert is_github is True
|
||||
assert owner == "owner"
|
||||
assert repo == "repo"
|
||||
assert api_url == "https://api.github.com/repos/owner/repo"
|
||||
|
||||
|
||||
def test_parse_github_url_non_github() -> None:
|
||||
url = "https://example.com/foo"
|
||||
api_url, is_github, owner, repo = parse_github_url(url)
|
||||
assert is_github is False
|
||||
assert owner is None
|
||||
assert repo is None
|
||||
assert api_url == url
|
||||
|
||||
|
||||
def test_parse_github_resource_url_file_and_raw() -> None:
|
||||
url = "https://github.com/owner/repo/blob/main/dir/file.txt"
|
||||
parsed = parse_github_resource_url(url)
|
||||
assert parsed == {
|
||||
"type": "file",
|
||||
"owner": "owner",
|
||||
"repo": "repo",
|
||||
"branch": "main",
|
||||
"path": "dir/file.txt",
|
||||
}
|
||||
|
||||
raw_url = "https://github.com/owner/repo/raw/main/file.txt"
|
||||
parsed_raw = parse_github_resource_url(raw_url)
|
||||
assert parsed_raw == {
|
||||
"type": "file",
|
||||
"owner": "owner",
|
||||
"repo": "repo",
|
||||
"branch": "main",
|
||||
"path": "file.txt",
|
||||
}
|
||||
|
||||
|
||||
def test_parse_github_resource_url_dir_repo_gist() -> None:
|
||||
dir_url = "https://github.com/owner/repo/tree/main/docs"
|
||||
parsed_dir = parse_github_resource_url(dir_url)
|
||||
assert parsed_dir == {
|
||||
"type": "dir",
|
||||
"owner": "owner",
|
||||
"repo": "repo",
|
||||
"branch": "main",
|
||||
"path": "docs",
|
||||
}
|
||||
|
||||
repo_url = "https://github.com/owner/repo"
|
||||
parsed_repo = parse_github_resource_url(repo_url)
|
||||
assert parsed_repo == {"type": "repo", "owner": "owner", "repo": "repo"}
|
||||
|
||||
gist_url = "https://gist.github.com/owner/abcdef"
|
||||
parsed_gist = parse_github_resource_url(gist_url)
|
||||
assert parsed_gist == {"type": "gist", "owner": "owner", "gist_id": "abcdef"}
|
||||
|
||||
|
||||
def test_parse_github_resource_url_normalizes_repo_name() -> None:
|
||||
repo_url = "https://github.com/owner/repo.git"
|
||||
parsed = parse_github_resource_url(repo_url)
|
||||
assert parsed == {"type": "repo", "owner": "owner", "repo": "repo"}
|
||||
|
||||
|
||||
def test_parse_github_resource_url_non_github() -> None:
|
||||
assert parse_github_resource_url("https://example.com") is None
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for generating alternative README outputs for root styles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.readme.generators.base import ReadmeGenerator # noqa: E402
|
||||
from scripts.readme.helpers import readme_config # noqa: E402
|
||||
from scripts.readme.helpers.readme_paths import resolve_asset_tokens # noqa: E402
|
||||
from scripts.readme.markup import visual as visual_markup # noqa: E402
|
||||
|
||||
|
||||
class DummyReadmeGenerator(ReadmeGenerator):
|
||||
"""Minimal generator for output path tests."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
csv_path: str,
|
||||
template_dir: str,
|
||||
assets_dir: str,
|
||||
repo_root: str,
|
||||
style_id: str,
|
||||
output_filename: str,
|
||||
template_filename: str,
|
||||
) -> None:
|
||||
super().__init__(csv_path, template_dir, assets_dir, repo_root)
|
||||
self._style_id = style_id
|
||||
self._output_filename = output_filename
|
||||
self._template_filename = template_filename
|
||||
|
||||
@property
|
||||
def template_filename(self) -> str:
|
||||
return self._template_filename
|
||||
|
||||
@property
|
||||
def output_filename(self) -> str:
|
||||
return self._output_filename
|
||||
|
||||
@property
|
||||
def style_id(self) -> str:
|
||||
return self._style_id
|
||||
|
||||
def load_csv_data(self) -> list[dict]:
|
||||
return []
|
||||
|
||||
def load_categories(self) -> list[dict]:
|
||||
return []
|
||||
|
||||
def load_overrides(self) -> dict:
|
||||
return {}
|
||||
|
||||
def load_announcements(self) -> str:
|
||||
return ""
|
||||
|
||||
def load_footer(self) -> str:
|
||||
return ""
|
||||
|
||||
def build_general_anchor_map(self) -> dict:
|
||||
return {}
|
||||
|
||||
def format_resource_entry(self, row: dict, include_separator: bool = True) -> str:
|
||||
_ = row, include_separator
|
||||
return ""
|
||||
|
||||
def generate_toc(self) -> str:
|
||||
return ""
|
||||
|
||||
def generate_weekly_section(self) -> str:
|
||||
return ""
|
||||
|
||||
def generate_section_content(self, category: dict, section_index: int) -> str:
|
||||
_ = category, section_index
|
||||
return ""
|
||||
|
||||
|
||||
def _write_template(template_dir: Path, filename: str) -> None:
|
||||
template_dir.mkdir(parents=True, exist_ok=True)
|
||||
(template_dir / filename).write_text(
|
||||
"{{STYLE_SELECTOR}}\n<img src=\"{{ASSET_PATH('logo.svg')}}\">",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _write_pyproject(repo_root: Path) -> None:
|
||||
(repo_root / "pyproject.toml").write_text("[tool]\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _configure_styles(monkeypatch: pytest.MonkeyPatch, root_style: str) -> None:
|
||||
monkeypatch.setitem(readme_config.CONFIG, "readme", {"root_style": root_style})
|
||||
monkeypatch.setitem(
|
||||
readme_config.CONFIG,
|
||||
"styles",
|
||||
{
|
||||
"extra": {
|
||||
"name": "Extra",
|
||||
"badge": "badge-style-extra.svg",
|
||||
"highlight_color": "#000000",
|
||||
"filename": "README_EXTRA.md",
|
||||
},
|
||||
"classic": {
|
||||
"name": "Classic",
|
||||
"badge": "badge-style-classic.svg",
|
||||
"highlight_color": "#000000",
|
||||
"filename": "README_CLASSIC.md",
|
||||
},
|
||||
},
|
||||
)
|
||||
monkeypatch.setitem(readme_config.CONFIG, "style_order", ["extra", "classic"])
|
||||
|
||||
|
||||
def test_root_classic_creates_alternative_copy(tmp_path: Path, monkeypatch) -> None:
|
||||
_configure_styles(monkeypatch, root_style="classic")
|
||||
_write_pyproject(tmp_path)
|
||||
|
||||
template_dir = tmp_path / "templates"
|
||||
_write_template(template_dir, "README_CLASSIC.template.md")
|
||||
(tmp_path / "assets").mkdir()
|
||||
|
||||
generator = DummyReadmeGenerator(
|
||||
csv_path=str(tmp_path / "data.csv"),
|
||||
template_dir=str(template_dir),
|
||||
assets_dir=str(tmp_path / "assets"),
|
||||
repo_root=str(tmp_path),
|
||||
style_id="classic",
|
||||
output_filename="README_ALTERNATIVES/README_CLASSIC.md",
|
||||
template_filename="README_CLASSIC.template.md",
|
||||
)
|
||||
|
||||
generator.generate()
|
||||
generator.generate(output_path="README.md")
|
||||
|
||||
root_readme = tmp_path / "README.md"
|
||||
alt_readme = tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md"
|
||||
|
||||
assert root_readme.exists()
|
||||
assert alt_readme.exists()
|
||||
|
||||
root_content = root_readme.read_text(encoding="utf-8")
|
||||
alt_content = alt_readme.read_text(encoding="utf-8")
|
||||
|
||||
assert 'src="assets/' in root_content
|
||||
assert 'src="../assets/' not in root_content
|
||||
assert 'src="../assets/' in alt_content
|
||||
|
||||
|
||||
def test_root_extra_creates_alternative_copy(tmp_path: Path, monkeypatch) -> None:
|
||||
_configure_styles(monkeypatch, root_style="extra")
|
||||
_write_pyproject(tmp_path)
|
||||
|
||||
template_dir = tmp_path / "templates"
|
||||
_write_template(template_dir, "README_EXTRA.template.md")
|
||||
(tmp_path / "assets").mkdir()
|
||||
|
||||
generator = DummyReadmeGenerator(
|
||||
csv_path=str(tmp_path / "data.csv"),
|
||||
template_dir=str(template_dir),
|
||||
assets_dir=str(tmp_path / "assets"),
|
||||
repo_root=str(tmp_path),
|
||||
style_id="extra",
|
||||
output_filename="README_ALTERNATIVES/README_EXTRA.md",
|
||||
template_filename="README_EXTRA.template.md",
|
||||
)
|
||||
|
||||
generator.generate()
|
||||
generator.generate(output_path="README.md")
|
||||
|
||||
root_readme = tmp_path / "README.md"
|
||||
alt_readme = tmp_path / "README_ALTERNATIVES" / "README_EXTRA.md"
|
||||
|
||||
assert root_readme.exists()
|
||||
assert alt_readme.exists()
|
||||
|
||||
root_content = root_readme.read_text(encoding="utf-8")
|
||||
alt_content = alt_readme.read_text(encoding="utf-8")
|
||||
|
||||
assert 'src="assets/' in root_content
|
||||
assert 'src="../assets/' not in root_content
|
||||
assert 'src="../assets/' in alt_content
|
||||
|
||||
|
||||
def test_visual_weekly_section_uses_asset_prefix(tmp_path: Path) -> None:
|
||||
assets_dir = tmp_path / "assets"
|
||||
assets_dir.mkdir()
|
||||
|
||||
csv_data = [
|
||||
{
|
||||
"Display Name": "Test Resource",
|
||||
"Primary Link": "https://github.com/example/repo",
|
||||
"Author Name": "Author",
|
||||
"Description": "Description",
|
||||
"Removed From Origin": "FALSE",
|
||||
"Date Added": "2025-01-01",
|
||||
}
|
||||
]
|
||||
|
||||
output = visual_markup.generate_weekly_section(csv_data, assets_dir=str(assets_dir))
|
||||
resolved = resolve_asset_tokens(
|
||||
output,
|
||||
tmp_path / "README_ALTERNATIVES" / "README_EXTRA.md",
|
||||
tmp_path,
|
||||
)
|
||||
|
||||
assert 'src="../assets/' in resolved
|
||||
assert 'srcset="../assets/' in resolved
|
||||
assert 'src="assets/' not in resolved
|
||||
assert 'srcset="assets/' not in resolved
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Tests for readme config file resolution."""
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(repo_root))
|
||||
|
||||
|
||||
from scripts.readme.helpers import readme_config # noqa: E402
|
||||
|
||||
|
||||
def _load_root_style(repo_root: Path) -> str:
|
||||
config_path = repo_root / "acc-config.yaml"
|
||||
with config_path.open(encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
return config.get("readme", {}).get("root_style", "extra")
|
||||
|
||||
|
||||
def test_load_config_uses_repo_root() -> None:
|
||||
"""load_config should read acc-config.yaml from repo root, not scripts/."""
|
||||
root_style = _load_root_style(repo_root)
|
||||
conflicting_root = "classic" if root_style != "classic" else "extra"
|
||||
|
||||
scripts_config_path = repo_root / "scripts" / "acc-config.yaml"
|
||||
existing_contents = None
|
||||
if scripts_config_path.exists():
|
||||
existing_contents = scripts_config_path.read_text(encoding="utf-8")
|
||||
|
||||
try:
|
||||
scripts_config = {"readme": {"root_style": conflicting_root}}
|
||||
scripts_config_path.write_text(
|
||||
yaml.safe_dump(scripts_config, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = readme_config.load_config()
|
||||
assert config.get("readme", {}).get("root_style", "extra") == root_style
|
||||
finally:
|
||||
if existing_contents is None:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
scripts_config_path.unlink()
|
||||
else:
|
||||
scripts_config_path.write_text(existing_contents, encoding="utf-8")
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for minimal and visual README generators."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.readme.generators import minimal as minimal_module # noqa: E402
|
||||
from scripts.readme.generators import visual as visual_module # noqa: E402
|
||||
from scripts.readme.generators.minimal import MinimalReadmeGenerator # noqa: E402
|
||||
from scripts.readme.generators.visual import VisualReadmeGenerator # noqa: E402
|
||||
|
||||
|
||||
def test_minimal_generator_properties() -> None:
|
||||
generator = MinimalReadmeGenerator("csv", "templates", "assets", "repo")
|
||||
assert generator.template_filename == "README_CLASSIC.template.md"
|
||||
assert generator.output_filename == "README_ALTERNATIVES/README_CLASSIC.md"
|
||||
assert generator.style_id == "classic"
|
||||
|
||||
|
||||
def test_minimal_generator_delegates(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
generator = MinimalReadmeGenerator("csv", "templates", "assets", "repo")
|
||||
generator.categories = [{"id": "cat"}]
|
||||
generator.csv_data = [{"Display Name": "Item"}]
|
||||
|
||||
calls: dict[str, object] = {}
|
||||
|
||||
def fake_format(row: dict, include_separator: bool = True) -> str:
|
||||
calls["format"] = (row, include_separator)
|
||||
return "ENTRY"
|
||||
|
||||
def fake_toc(categories: list[dict], csv_data: list[dict]) -> str:
|
||||
calls["toc"] = (categories, csv_data)
|
||||
return "TOC"
|
||||
|
||||
def fake_weekly(csv_data: list[dict]) -> str:
|
||||
calls["weekly"] = (csv_data,)
|
||||
return "WEEKLY"
|
||||
|
||||
def fake_section(category: dict, csv_data: list[dict]) -> str:
|
||||
calls["section"] = (category, csv_data)
|
||||
return "SECTION"
|
||||
|
||||
monkeypatch.setattr(minimal_module, "format_minimal_resource_entry", fake_format)
|
||||
monkeypatch.setattr(minimal_module, "generate_minimal_toc", fake_toc)
|
||||
monkeypatch.setattr(minimal_module, "generate_minimal_weekly_section", fake_weekly)
|
||||
monkeypatch.setattr(minimal_module, "generate_minimal_section_content", fake_section)
|
||||
|
||||
assert generator.format_resource_entry({"id": 1}, include_separator=False) == "ENTRY"
|
||||
assert calls["format"] == ({"id": 1}, False)
|
||||
|
||||
assert generator.generate_toc() == "TOC"
|
||||
assert calls["toc"] == (generator.categories, generator.csv_data)
|
||||
|
||||
assert generator.generate_weekly_section() == "WEEKLY"
|
||||
assert calls["weekly"] == (generator.csv_data,)
|
||||
|
||||
assert generator.generate_section_content({"id": "cat"}, section_index=3) == "SECTION"
|
||||
assert calls["section"] == ({"id": "cat"}, generator.csv_data)
|
||||
|
||||
|
||||
def test_visual_generator_properties() -> None:
|
||||
generator = VisualReadmeGenerator("csv", "templates", "assets", "repo")
|
||||
assert generator.template_filename == "README_EXTRA.template.md"
|
||||
assert generator.output_filename == "README_ALTERNATIVES/README_EXTRA.md"
|
||||
assert generator.style_id == "extra"
|
||||
|
||||
|
||||
def test_visual_generator_delegates(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
generator = VisualReadmeGenerator("csv", "templates", "assets", "repo")
|
||||
generator.categories = [{"id": "cat"}]
|
||||
generator.csv_data = [{"Display Name": "Item"}]
|
||||
generator.general_anchor_map = {"General": "general"}
|
||||
generator.assets_dir = "/assets"
|
||||
|
||||
calls: dict[str, object] = {}
|
||||
|
||||
def fake_format(row: dict, assets_dir: str, include_separator: bool = True) -> str:
|
||||
calls["format"] = (row, assets_dir, include_separator)
|
||||
return "ENTRY"
|
||||
|
||||
def fake_toc(categories: list[dict], csv_data: list[dict], anchor_map: dict) -> str:
|
||||
calls["toc"] = (categories, csv_data, anchor_map)
|
||||
return "TOC"
|
||||
|
||||
def fake_weekly(csv_data: list[dict], assets_dir: str) -> str:
|
||||
calls["weekly"] = (csv_data, assets_dir)
|
||||
return "WEEKLY"
|
||||
|
||||
def fake_section(
|
||||
category: dict,
|
||||
csv_data: list[dict],
|
||||
anchor_map: dict,
|
||||
assets_dir: str,
|
||||
section_index: int,
|
||||
) -> str:
|
||||
calls["section"] = (category, csv_data, anchor_map, assets_dir, section_index)
|
||||
return "SECTION"
|
||||
|
||||
def fake_ticker() -> str:
|
||||
calls["ticker"] = True
|
||||
return "TICKER"
|
||||
|
||||
monkeypatch.setattr(visual_module, "format_visual_resource_entry", fake_format)
|
||||
monkeypatch.setattr(visual_module, "generate_visual_toc", fake_toc)
|
||||
monkeypatch.setattr(visual_module, "generate_visual_weekly_section", fake_weekly)
|
||||
monkeypatch.setattr(visual_module, "generate_visual_section_content", fake_section)
|
||||
monkeypatch.setattr(visual_module, "generate_visual_repo_ticker", fake_ticker)
|
||||
|
||||
assert generator.format_resource_entry({"id": 1}, include_separator=False) == "ENTRY"
|
||||
assert calls["format"] == ({"id": 1}, "/assets", False)
|
||||
|
||||
assert generator.generate_toc() == "TOC"
|
||||
assert calls["toc"] == (generator.categories, generator.csv_data, generator.general_anchor_map)
|
||||
|
||||
assert generator.generate_weekly_section() == "WEEKLY"
|
||||
assert calls["weekly"] == (generator.csv_data, "/assets")
|
||||
|
||||
assert generator.generate_section_content({"id": "cat"}, section_index=2) == "SECTION"
|
||||
assert calls["section"] == (
|
||||
{"id": "cat"},
|
||||
generator.csv_data,
|
||||
generator.general_anchor_map,
|
||||
"/assets",
|
||||
2,
|
||||
)
|
||||
|
||||
assert generator.generate_repo_ticker() == "TICKER"
|
||||
assert calls["ticker"] is True
|
||||
264
.agent/knowledge/awesome_claude/tests/test_resource_utils.py
Normal file
264
.agent/knowledge/awesome_claude/tests/test_resource_utils.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for append_to_csv in resource_utils.py.
|
||||
|
||||
Tests cover:
|
||||
- append_to_csv function with all columns including release metadata
|
||||
- CSV column alignment
|
||||
- Default values for new resources
|
||||
"""
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path to import the script
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from scripts.resources.resource_utils import append_to_csv # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_csv() -> Generator[Path, None, None]:
|
||||
"""Create a temporary CSV file for testing."""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, newline="") as f:
|
||||
temp_path = Path(f.name)
|
||||
# Write header row matching the actual CSV structure
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(
|
||||
[
|
||||
"ID",
|
||||
"Display Name",
|
||||
"Category",
|
||||
"Sub-Category",
|
||||
"Primary Link",
|
||||
"Secondary Link",
|
||||
"Author Name",
|
||||
"Author Link",
|
||||
"Active",
|
||||
"Date Added",
|
||||
"Last Modified",
|
||||
"Last Checked",
|
||||
"License",
|
||||
"Description",
|
||||
"Removed From Origin",
|
||||
"Stale",
|
||||
"Repo Created",
|
||||
"Latest Release",
|
||||
"Release Version",
|
||||
"Release Source",
|
||||
]
|
||||
)
|
||||
yield temp_path
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_resource_data() -> dict[str, str]:
|
||||
"""Sample resource data for testing."""
|
||||
return {
|
||||
"id": "test-resource-001",
|
||||
"display_name": "Test Resource",
|
||||
"category": "Tooling",
|
||||
"subcategory": "General",
|
||||
"primary_link": "https://example.com/test",
|
||||
"secondary_link": "https://example.com/secondary",
|
||||
"author_name": "Test Author",
|
||||
"author_link": "https://github.com/testauthor",
|
||||
"license": "MIT",
|
||||
"description": "A test resource for unit testing",
|
||||
}
|
||||
|
||||
|
||||
def set_csv_path(monkeypatch: pytest.MonkeyPatch, path: Path | str) -> None:
|
||||
"""Force append_to_csv to write to a specific path."""
|
||||
monkeypatch.setattr("scripts.resources.resource_utils.os.path.join", lambda *args: str(path))
|
||||
|
||||
|
||||
def test_append_to_csv_adds_all_columns(
|
||||
temp_csv: Path, sample_resource_data: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that append_to_csv adds a row with all 20 columns."""
|
||||
set_csv_path(monkeypatch, temp_csv)
|
||||
|
||||
# Append the resource
|
||||
result = append_to_csv(sample_resource_data)
|
||||
assert result is True
|
||||
|
||||
# Read back the CSV and verify
|
||||
with open(temp_csv, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
header = next(reader)
|
||||
assert len(header) == 20, f"Expected 20 columns in header, got {len(header)}"
|
||||
|
||||
# Read the data row
|
||||
data_row = next(reader)
|
||||
assert len(data_row) == 20, f"Expected 20 columns in data row, got {len(data_row)}"
|
||||
|
||||
# Verify specific column values
|
||||
assert data_row[0] == sample_resource_data["id"]
|
||||
assert data_row[1] == sample_resource_data["display_name"]
|
||||
assert data_row[2] == sample_resource_data["category"]
|
||||
assert data_row[3] == sample_resource_data["subcategory"]
|
||||
assert data_row[4] == sample_resource_data["primary_link"]
|
||||
assert data_row[5] == sample_resource_data["secondary_link"]
|
||||
assert data_row[6] == sample_resource_data["author_name"]
|
||||
assert data_row[7] == sample_resource_data["author_link"]
|
||||
assert data_row[8] == "TRUE" # Active default
|
||||
assert data_row[12] == sample_resource_data["license"]
|
||||
assert data_row[13] == sample_resource_data["description"]
|
||||
assert data_row[14] == "FALSE" # Removed From Origin default
|
||||
assert data_row[15] == "FALSE" # Stale default
|
||||
assert data_row[16] == "" # Repo Created default
|
||||
assert data_row[17] == "" # Latest Release default
|
||||
assert data_row[18] == "" # Release Version default
|
||||
assert data_row[19] == "" # Release Source default
|
||||
|
||||
|
||||
def test_append_to_csv_default_values(
|
||||
temp_csv: Path, sample_resource_data: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that append_to_csv uses correct default values."""
|
||||
set_csv_path(monkeypatch, temp_csv)
|
||||
|
||||
# Append the resource
|
||||
result = append_to_csv(sample_resource_data)
|
||||
assert result is True
|
||||
|
||||
# Read back and check defaults
|
||||
with open(temp_csv, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # Skip header
|
||||
data_row = next(reader)
|
||||
|
||||
# Check default values
|
||||
assert data_row[8] == "TRUE", "Active should default to TRUE"
|
||||
assert data_row[10] == "", "Last Modified should default to empty"
|
||||
assert data_row[14] == "FALSE", "Removed From Origin should default to FALSE"
|
||||
assert data_row[15] == "FALSE", "Stale should default to FALSE"
|
||||
assert data_row[16] == "", "Repo Created should default to empty"
|
||||
assert data_row[17] == "", "Latest Release should default to empty"
|
||||
assert data_row[18] == "", "Release Version should default to empty"
|
||||
assert data_row[19] == "", "Release Source should default to empty"
|
||||
|
||||
|
||||
def test_append_to_csv_with_removed_from_origin_true(
|
||||
temp_csv: Path, sample_resource_data: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that append_to_csv respects removed_from_origin when provided."""
|
||||
sample_resource_data["removed_from_origin"] = "TRUE"
|
||||
|
||||
set_csv_path(monkeypatch, temp_csv)
|
||||
|
||||
# Append the resource
|
||||
result = append_to_csv(sample_resource_data)
|
||||
assert result is True
|
||||
|
||||
# Read back and verify
|
||||
with open(temp_csv, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # Skip header
|
||||
data_row = next(reader)
|
||||
|
||||
assert data_row[14] == "TRUE", "Removed From Origin should be TRUE when provided"
|
||||
|
||||
|
||||
def test_append_to_csv_date_fields(
|
||||
temp_csv: Path, sample_resource_data: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that append_to_csv sets date fields correctly."""
|
||||
set_csv_path(monkeypatch, temp_csv)
|
||||
|
||||
# Capture the current time window
|
||||
before_time = datetime.now()
|
||||
result = append_to_csv(sample_resource_data)
|
||||
after_time = datetime.now()
|
||||
|
||||
assert result is True
|
||||
|
||||
# Read back and check date fields
|
||||
with open(temp_csv, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # Skip header
|
||||
data_row = next(reader)
|
||||
|
||||
# Parse the date fields
|
||||
date_added = datetime.strptime(data_row[9], "%Y-%m-%d:%H-%M-%S")
|
||||
last_checked = datetime.strptime(data_row[11], "%Y-%m-%d:%H-%M-%S")
|
||||
|
||||
# Verify dates are within the expected time window (account for second precision)
|
||||
# The strptime loses microseconds, so we need to compare at second precision
|
||||
assert (
|
||||
before_time.replace(microsecond=0)
|
||||
<= date_added
|
||||
<= after_time.replace(microsecond=0) + timedelta(seconds=1)
|
||||
), "Date Added should be current time"
|
||||
assert (
|
||||
before_time.replace(microsecond=0)
|
||||
<= last_checked
|
||||
<= after_time.replace(microsecond=0) + timedelta(seconds=1)
|
||||
), "Last Checked should be current time"
|
||||
|
||||
|
||||
def test_append_to_csv_handles_csv_error(
|
||||
sample_resource_data: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that append_to_csv handles file write errors gracefully."""
|
||||
# Point to a non-writable location
|
||||
set_csv_path(monkeypatch, "/invalid/path/to/csv.csv")
|
||||
|
||||
# Should return False on error
|
||||
result = append_to_csv(sample_resource_data)
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_append_to_csv_preserves_existing_data(
|
||||
temp_csv: Path, sample_resource_data: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that append_to_csv appends without modifying existing rows."""
|
||||
# Add an existing row first
|
||||
with open(temp_csv, "a", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(
|
||||
[
|
||||
"existing-001",
|
||||
"Existing Resource",
|
||||
"Hooks",
|
||||
"General",
|
||||
"https://existing.com",
|
||||
"",
|
||||
"Existing Author",
|
||||
"https://github.com/existing",
|
||||
"TRUE",
|
||||
"2025-01-01:00-00-00",
|
||||
"",
|
||||
"2025-01-01:00-00-00",
|
||||
"Apache-2.0",
|
||||
"An existing resource",
|
||||
"FALSE",
|
||||
"FALSE",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
set_csv_path(monkeypatch, temp_csv)
|
||||
|
||||
# Append new resource
|
||||
result = append_to_csv(sample_resource_data)
|
||||
assert result is True
|
||||
|
||||
# Verify both rows exist
|
||||
with open(temp_csv, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
rows = list(reader)
|
||||
|
||||
assert len(rows) == 3, "Should have header + 2 data rows"
|
||||
assert rows[1][0] == "existing-001", "Existing row should be preserved"
|
||||
assert rows[2][0] == sample_resource_data["id"], "New row should be appended"
|
||||
598
.agent/knowledge/awesome_claude/tests/test_sort_resources.py
Normal file
598
.agent/knowledge/awesome_claude/tests/test_sort_resources.py
Normal file
@@ -0,0 +1,598 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for sort_resources.py script.
|
||||
|
||||
Tests cover:
|
||||
- Sorting by category order
|
||||
- Sorting by sub-category
|
||||
- Sorting by display name
|
||||
- Edge cases (empty CSV, missing fields)
|
||||
- Category summary generation
|
||||
"""
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path to import the script
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from scripts.resources.sort_resources import sort_resources # noqa
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_csv() -> Generator[Path, None, None]:
|
||||
"""Create a temporary CSV file for testing."""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, newline="") as f:
|
||||
temp_path = Path(f.name)
|
||||
yield temp_path
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_csv_data() -> list[dict[str, str]]:
|
||||
"""Sample CSV data for testing."""
|
||||
return [
|
||||
{
|
||||
"ID": "cmd-001",
|
||||
"Display Name": "Zebra Command",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "Version Control & Git",
|
||||
"Primary Link": "https://example.com/zebra",
|
||||
"Author Name": "Author Z",
|
||||
"Author Link": "https://github.com/authorz",
|
||||
"Description": "Last alphabetically",
|
||||
},
|
||||
{
|
||||
"ID": "tool-001",
|
||||
"Display Name": "Alpha Tool",
|
||||
"Category": "Tooling",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/alpha",
|
||||
"Author Name": "Author A",
|
||||
"Author Link": "https://github.com/authora",
|
||||
"Description": "First alphabetically",
|
||||
},
|
||||
{
|
||||
"ID": "cmd-002",
|
||||
"Display Name": "Beta Command",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "Code Analysis & Testing",
|
||||
"Primary Link": "https://example.com/beta",
|
||||
"Author Name": "Author B",
|
||||
"Author Link": "https://github.com/authorb",
|
||||
"Description": "Second alphabetically",
|
||||
},
|
||||
{
|
||||
"ID": "wf-001",
|
||||
"Display Name": "Charlie Workflow",
|
||||
"Category": "Workflows & Knowledge Guides",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/charlie",
|
||||
"Author Name": "Author C",
|
||||
"Author Link": "https://github.com/authorc",
|
||||
"Description": "Third alphabetically",
|
||||
},
|
||||
{
|
||||
"ID": "cmd-003",
|
||||
"Display Name": "Alpha Command",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "Code Analysis & Testing",
|
||||
"Primary Link": "https://example.com/alphacmd",
|
||||
"Author Name": "Author AC",
|
||||
"Author Link": "https://github.com/authorac",
|
||||
"Description": "Should sort before Beta in same subcategory",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def write_csv(path: Path, data: list[dict[str, str]]) -> None:
|
||||
"""Helper to write CSV data to a file."""
|
||||
if not data:
|
||||
path.write_text("")
|
||||
return
|
||||
|
||||
with open(path, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=data[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(data)
|
||||
|
||||
|
||||
def read_csv(path: Path) -> list[dict[str, str]]:
|
||||
"""Helper to read CSV data from a file."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def set_category_order(monkeypatch: pytest.MonkeyPatch, categories: list[dict[str, Any]]) -> None:
|
||||
"""Override category order for sorting tests."""
|
||||
monkeypatch.setattr(
|
||||
"scripts.categories.category_utils.category_manager.get_categories_for_readme",
|
||||
lambda: categories,
|
||||
)
|
||||
|
||||
|
||||
def set_category_order_error(
|
||||
monkeypatch: pytest.MonkeyPatch, message: str = "Category manager error"
|
||||
) -> None:
|
||||
"""Force category manager to raise an error."""
|
||||
|
||||
def _raise() -> None:
|
||||
raise Exception(message)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"scripts.categories.category_utils.category_manager.get_categories_for_readme",
|
||||
_raise,
|
||||
)
|
||||
|
||||
|
||||
class TestSortResources:
|
||||
"""Test cases for sort_resources function."""
|
||||
|
||||
def test_sort_by_category_order(
|
||||
self,
|
||||
temp_csv: Path,
|
||||
sample_csv_data: list[dict[str, str]],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that resources are sorted according to category order from category_utils."""
|
||||
# Mock category manager to provide a specific order
|
||||
mock_categories = [
|
||||
{"name": "Workflows & Knowledge Guides"},
|
||||
{"name": "Tooling"},
|
||||
{"name": "Slash-Commands"},
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, mock_categories)
|
||||
write_csv(temp_csv, sample_csv_data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
categories = [row["Category"] for row in sorted_data]
|
||||
|
||||
# Check that categories appear in the specified order
|
||||
assert categories[0] == "Workflows & Knowledge Guides"
|
||||
assert categories[1] == "Tooling"
|
||||
assert categories[2:] == ["Slash-Commands"] * 3
|
||||
|
||||
def test_sort_by_subcategory(
|
||||
self,
|
||||
temp_csv: Path,
|
||||
sample_csv_data: list[dict[str, str]],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that resources within a category are sorted by sub-category."""
|
||||
set_category_order(monkeypatch, [{"name": "Slash-Commands"}])
|
||||
# Filter to just Slash-Commands for this test
|
||||
slash_commands = [d for d in sample_csv_data if d["Category"] == "Slash-Commands"]
|
||||
write_csv(temp_csv, slash_commands)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
subcategories = [row["Sub-Category"] for row in sorted_data]
|
||||
|
||||
# "Code Analysis & Testing" should come before "Version Control & Git"
|
||||
assert subcategories[0] == "Code Analysis & Testing"
|
||||
assert subcategories[1] == "Code Analysis & Testing"
|
||||
assert subcategories[2] == "Version Control & Git"
|
||||
|
||||
def test_sort_by_display_name(self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test that resources within same category/subcategory are sorted by display name."""
|
||||
data = [
|
||||
{
|
||||
"ID": "cmd-003",
|
||||
"Display Name": "Zebra",
|
||||
"Category": "Same",
|
||||
"Sub-Category": "Same",
|
||||
"Primary Link": "https://example.com/z",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Z",
|
||||
},
|
||||
{
|
||||
"ID": "cmd-001",
|
||||
"Display Name": "Alpha",
|
||||
"Category": "Same",
|
||||
"Sub-Category": "Same",
|
||||
"Primary Link": "https://example.com/a",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "A",
|
||||
},
|
||||
{
|
||||
"ID": "cmd-002",
|
||||
"Display Name": "Beta",
|
||||
"Category": "Same",
|
||||
"Sub-Category": "Same",
|
||||
"Primary Link": "https://example.com/b",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "B",
|
||||
},
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, [{"name": "Same"}])
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
names = [row["Display Name"] for row in sorted_data]
|
||||
|
||||
assert names == ["Alpha", "Beta", "Zebra"]
|
||||
|
||||
def test_empty_subcategory_sorts_last(
|
||||
self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that empty sub-categories sort after filled ones."""
|
||||
data = [
|
||||
{
|
||||
"ID": "1",
|
||||
"Display Name": "No Subcat",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/1",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Empty subcat",
|
||||
},
|
||||
{
|
||||
"ID": "2",
|
||||
"Display Name": "Has Subcat",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "Subcategory A",
|
||||
"Primary Link": "https://example.com/2",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Has subcat",
|
||||
},
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, [{"name": "Test"}])
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
|
||||
# Item with subcategory should come first
|
||||
assert sorted_data[0]["Sub-Category"] == "Subcategory A"
|
||||
assert sorted_data[1]["Sub-Category"] == ""
|
||||
|
||||
def test_unknown_category_sorts_last(
|
||||
self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that categories not in the predefined order sort last."""
|
||||
data = [
|
||||
{
|
||||
"ID": "1",
|
||||
"Display Name": "Unknown Cat",
|
||||
"Category": "Unknown Category",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/1",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Unknown",
|
||||
},
|
||||
{
|
||||
"ID": "2",
|
||||
"Display Name": "Known Cat",
|
||||
"Category": "Known",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/2",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Known",
|
||||
},
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, [{"name": "Known"}])
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
|
||||
# Known category should come first
|
||||
assert sorted_data[0]["Category"] == "Known"
|
||||
assert sorted_data[1]["Category"] == "Unknown Category"
|
||||
|
||||
def test_subcategory_yaml_order_sort(
|
||||
self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that subcategories are sorted by their defined order in YAML."""
|
||||
data = [
|
||||
{
|
||||
"ID": "1",
|
||||
"Display Name": "A",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "Miscellaneous",
|
||||
"Primary Link": "https://example.com/1",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "M",
|
||||
},
|
||||
{
|
||||
"ID": "2",
|
||||
"Display Name": "B",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "Version Control & Git",
|
||||
"Primary Link": "https://example.com/2",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "V",
|
||||
},
|
||||
{
|
||||
"ID": "3",
|
||||
"Display Name": "C",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "General",
|
||||
"Primary Link": "https://example.com/3",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "G",
|
||||
},
|
||||
{
|
||||
"ID": "4",
|
||||
"Display Name": "D",
|
||||
"Category": "Slash-Commands",
|
||||
"Sub-Category": "CI / Deployment",
|
||||
"Primary Link": "https://example.com/4",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "C",
|
||||
},
|
||||
]
|
||||
|
||||
# Mock the categories with subcategories in specific order
|
||||
mock_categories = [
|
||||
{
|
||||
"name": "Slash-Commands",
|
||||
"subcategories": [
|
||||
{"name": "General"},
|
||||
{"name": "Version Control & Git"},
|
||||
{"name": "Code Analysis & Testing"},
|
||||
{"name": "Context Loading & Priming"},
|
||||
{"name": "Documentation & Changelogs"},
|
||||
{"name": "CI / Deployment"},
|
||||
{"name": "Project & Task Management"},
|
||||
{"name": "Miscellaneous"},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, mock_categories)
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
subcats = [row["Sub-Category"] for row in sorted_data]
|
||||
|
||||
# Should follow YAML order: General first, Version Control,
|
||||
# CI/Deployment, then Miscellaneous last
|
||||
assert subcats == [
|
||||
"General",
|
||||
"Version Control & Git",
|
||||
"CI / Deployment",
|
||||
"Miscellaneous",
|
||||
]
|
||||
|
||||
def test_case_insensitive_display_name_sort(
|
||||
self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that display name sorting is case-insensitive."""
|
||||
data = [
|
||||
{
|
||||
"ID": "1",
|
||||
"Display Name": "UPPERCASE",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/1",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Upper",
|
||||
},
|
||||
{
|
||||
"ID": "2",
|
||||
"Display Name": "lowercase",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/2",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Lower",
|
||||
},
|
||||
{
|
||||
"ID": "3",
|
||||
"Display Name": "MixedCase",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/3",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Mixed",
|
||||
},
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, [{"name": "Test"}])
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
names = [row["Display Name"] for row in sorted_data]
|
||||
|
||||
# Should be sorted alphabetically regardless of case
|
||||
assert names == ["lowercase", "MixedCase", "UPPERCASE"]
|
||||
|
||||
def test_empty_csv_file(self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test handling of empty CSV file."""
|
||||
# Create empty file
|
||||
temp_csv.write_text("")
|
||||
|
||||
set_category_order(monkeypatch, [])
|
||||
# Should not raise an error
|
||||
sort_resources(temp_csv)
|
||||
|
||||
# File should still be empty
|
||||
assert temp_csv.read_text() == ""
|
||||
|
||||
def test_missing_fields_handled_gracefully(
|
||||
self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that missing fields in CSV rows are handled gracefully."""
|
||||
data = [
|
||||
{
|
||||
"ID": "1",
|
||||
"Display Name": "Complete",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "Sub",
|
||||
"Primary Link": "https://example.com/1",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Complete row",
|
||||
},
|
||||
{
|
||||
"ID": "2",
|
||||
"Display Name": "Missing Category",
|
||||
# Missing Category field
|
||||
"Sub-Category": "Sub",
|
||||
"Primary Link": "https://example.com/2",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Missing category",
|
||||
},
|
||||
{
|
||||
"ID": "3",
|
||||
# Missing Display Name
|
||||
"Category": "Test",
|
||||
"Sub-Category": "Sub",
|
||||
"Primary Link": "https://example.com/3",
|
||||
"Author Name": "A",
|
||||
"Author Link": "https://github.com/a",
|
||||
"Description": "Missing name",
|
||||
},
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, [{"name": "Test"}])
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
|
||||
# Should handle missing fields without crashing
|
||||
assert len(sorted_data) == 3
|
||||
# Missing display name should sort as empty string (first)
|
||||
assert sorted_data[0]["ID"] == "3"
|
||||
|
||||
def test_category_manager_exception_handling(
|
||||
self,
|
||||
temp_csv: Path,
|
||||
sample_csv_data: list[dict[str, str]],
|
||||
capsys: Any,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that exceptions from category_manager are handled gracefully."""
|
||||
set_category_order_error(monkeypatch, "Category manager error")
|
||||
write_csv(temp_csv, sample_csv_data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
# Should still sort the file (alphabetically)
|
||||
sorted_data = read_csv(temp_csv)
|
||||
assert len(sorted_data) == len(sample_csv_data)
|
||||
|
||||
# Check that warning was printed
|
||||
captured = capsys.readouterr()
|
||||
assert "Warning: Could not load category order" in captured.out
|
||||
assert "Using alphabetical sorting instead" in captured.out
|
||||
|
||||
def test_preserve_all_csv_fields(self, temp_csv: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test that all CSV fields are preserved after sorting."""
|
||||
data = [
|
||||
{
|
||||
"ID": "1",
|
||||
"Display Name": "Test",
|
||||
"Category": "Test",
|
||||
"Sub-Category": "",
|
||||
"Primary Link": "https://example.com/1",
|
||||
"Author Name": "Author",
|
||||
"Author Link": "https://github.com/author",
|
||||
"Description": "Description",
|
||||
"Extra Field 1": "Extra Value 1",
|
||||
"Extra Field 2": "Extra Value 2",
|
||||
"Active": "true",
|
||||
"Last Checked": "2024-01-01",
|
||||
}
|
||||
]
|
||||
|
||||
set_category_order(monkeypatch, [{"name": "Test"}])
|
||||
write_csv(temp_csv, data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
sorted_data = read_csv(temp_csv)
|
||||
|
||||
# All fields should be preserved
|
||||
assert sorted_data[0]["Extra Field 1"] == "Extra Value 1"
|
||||
assert sorted_data[0]["Extra Field 2"] == "Extra Value 2"
|
||||
assert sorted_data[0]["Active"] == "true"
|
||||
assert sorted_data[0]["Last Checked"] == "2024-01-01"
|
||||
|
||||
def test_category_summary_output(
|
||||
self,
|
||||
temp_csv: Path,
|
||||
sample_csv_data: list[dict[str, str]],
|
||||
capsys: Any,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that category summary is printed correctly."""
|
||||
set_category_order(
|
||||
monkeypatch,
|
||||
[
|
||||
{"name": "Workflows & Knowledge Guides"},
|
||||
{"name": "Tooling"},
|
||||
{"name": "Slash-Commands"},
|
||||
],
|
||||
)
|
||||
write_csv(temp_csv, sample_csv_data)
|
||||
sort_resources(temp_csv)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
# Check summary output
|
||||
assert "Category Summary:" in captured.out
|
||||
assert "Workflows & Knowledge Guides:" in captured.out
|
||||
assert "(no sub-category): 1 items" in captured.out
|
||||
assert "Slash-Commands:" in captured.out
|
||||
assert "Code Analysis & Testing: 2 items" in captured.out
|
||||
assert "Version Control & Git: 1 items" in captured.out
|
||||
|
||||
def test_multiple_sort_stability(
|
||||
self,
|
||||
temp_csv: Path,
|
||||
sample_csv_data: list[dict[str, str]],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
Test that sorting multiple times produces the same result
|
||||
(stable sort).
|
||||
"""
|
||||
set_category_order(
|
||||
monkeypatch,
|
||||
[
|
||||
{"name": "Workflows & Knowledge Guides"},
|
||||
{"name": "Tooling"},
|
||||
{"name": "Slash-Commands"},
|
||||
],
|
||||
)
|
||||
write_csv(temp_csv, sample_csv_data)
|
||||
|
||||
# Sort once
|
||||
sort_resources(temp_csv)
|
||||
first_sort = read_csv(temp_csv)
|
||||
|
||||
# Sort again
|
||||
sort_resources(temp_csv)
|
||||
second_sort = read_csv(temp_csv)
|
||||
|
||||
# Results should be identical
|
||||
assert first_sort == second_sort
|
||||
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Tests for style selector and path handling logic.
|
||||
|
||||
These tests verify the brittle assumptions around:
|
||||
- output_path-based selector behavior
|
||||
- resolved_output_path computation
|
||||
- generate_style_selector() path generation
|
||||
- Config-driven root style switching
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add repo root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.readme.helpers import readme_config # noqa: E402
|
||||
from scripts.readme.helpers.readme_config import ( # noqa: E402
|
||||
CONFIG,
|
||||
get_root_style,
|
||||
get_style_selector_target,
|
||||
)
|
||||
from scripts.readme.helpers.readme_paths import ( # noqa: E402
|
||||
resolve_asset_tokens,
|
||||
resolve_relative_link,
|
||||
)
|
||||
from scripts.readme.markup.shared import generate_style_selector # noqa: E402
|
||||
|
||||
|
||||
def _resolve_selector(html: str, output_path: Path, repo_root: Path) -> str:
|
||||
return resolve_asset_tokens(html, output_path, repo_root)
|
||||
|
||||
|
||||
class TestResolveRelativeLink:
|
||||
"""Tests for resolve_relative_link() helper."""
|
||||
|
||||
def test_root_self_link_is_dot_slash(self, tmp_path: Path) -> None:
|
||||
href = resolve_relative_link(tmp_path / "README.md", Path("README.md"), tmp_path)
|
||||
assert href == "./"
|
||||
|
||||
def test_root_to_alternative_link(self, tmp_path: Path) -> None:
|
||||
href = resolve_relative_link(
|
||||
tmp_path / "README.md",
|
||||
Path("README_ALTERNATIVES/README_CLASSIC.md"),
|
||||
tmp_path,
|
||||
)
|
||||
assert href == "README_ALTERNATIVES/README_CLASSIC.md"
|
||||
|
||||
def test_alternative_to_root_link(self, tmp_path: Path) -> None:
|
||||
href = resolve_relative_link(
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
Path("README.md"),
|
||||
tmp_path,
|
||||
)
|
||||
assert href == "../"
|
||||
|
||||
def test_alternative_to_sibling_link(self, tmp_path: Path) -> None:
|
||||
href = resolve_relative_link(
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
Path("README_ALTERNATIVES/README_AWESOME.md"),
|
||||
tmp_path,
|
||||
)
|
||||
assert href == "README_AWESOME.md"
|
||||
|
||||
|
||||
class TestGetRootStyle:
|
||||
"""Tests for get_root_style() function."""
|
||||
|
||||
def test_root_style_from_config(self) -> None:
|
||||
"""Root style should come from config."""
|
||||
root_style = get_root_style()
|
||||
assert root_style in ["extra", "classic", "awesome", "flat"]
|
||||
|
||||
def test_root_style_can_be_changed(self, monkeypatch) -> None:
|
||||
"""Root style should reflect config changes."""
|
||||
monkeypatch.setitem(readme_config.CONFIG, "readme", {"root_style": "awesome"})
|
||||
assert get_root_style() == "awesome"
|
||||
|
||||
def test_root_style_fallback(self, monkeypatch) -> None:
|
||||
"""Should fall back to 'extra' if not configured."""
|
||||
monkeypatch.setitem(readme_config.CONFIG, "readme", {})
|
||||
assert get_root_style() == "extra"
|
||||
|
||||
def test_root_style_missing_readme_section(self, monkeypatch) -> None:
|
||||
"""Should fall back to 'extra' if readme section missing."""
|
||||
monkeypatch.delitem(readme_config.CONFIG, "readme", raising=False)
|
||||
assert get_root_style() == "extra"
|
||||
|
||||
|
||||
class TestGetStyleSelectorTarget:
|
||||
"""Tests for get_style_selector_target() function."""
|
||||
|
||||
def test_root_style_goes_to_root(self) -> None:
|
||||
"""The root style should always output to README.md."""
|
||||
root_style = get_root_style()
|
||||
path = get_style_selector_target(root_style)
|
||||
assert path == "README.md"
|
||||
|
||||
def test_non_root_style_goes_to_alternatives(self) -> None:
|
||||
"""Non-root styles should go to README_ALTERNATIVES/."""
|
||||
root_style = get_root_style()
|
||||
non_root_styles = [s for s in ["extra", "classic", "awesome", "flat"] if s != root_style]
|
||||
|
||||
for style in non_root_styles:
|
||||
path = get_style_selector_target(style)
|
||||
assert path.startswith("README_ALTERNATIVES/"), (
|
||||
f"Style '{style}' should be in README_ALTERNATIVES/, got: {path}"
|
||||
)
|
||||
|
||||
def test_style_swap_extra_to_alternatives(self, monkeypatch) -> None:
|
||||
"""When awesome is root, extra should move to alternatives."""
|
||||
monkeypatch.setitem(readme_config.CONFIG, "readme", {"root_style": "awesome"})
|
||||
monkeypatch.setitem(
|
||||
readme_config.CONFIG,
|
||||
"styles",
|
||||
{
|
||||
"extra": {"filename": "README_EXTRA.md"},
|
||||
"awesome": {"filename": "README_AWESOME.md"},
|
||||
},
|
||||
)
|
||||
assert get_style_selector_target("awesome") == "README.md"
|
||||
assert get_style_selector_target("extra") == "README_ALTERNATIVES/README_EXTRA.md"
|
||||
|
||||
def test_style_originally_in_alternatives_becomes_root(self, monkeypatch) -> None:
|
||||
"""A style configured for alternatives can become root."""
|
||||
monkeypatch.setitem(readme_config.CONFIG, "readme", {"root_style": "classic"})
|
||||
monkeypatch.setitem(
|
||||
readme_config.CONFIG,
|
||||
"styles",
|
||||
{
|
||||
"classic": {"filename": "README_CLASSIC.md"},
|
||||
"extra": {"filename": "README_EXTRA.md"},
|
||||
},
|
||||
)
|
||||
assert get_style_selector_target("classic") == "README.md"
|
||||
|
||||
|
||||
class TestGenerateStyleSelector:
|
||||
"""Tests for generate_style_selector() function."""
|
||||
|
||||
def test_root_readme_uses_assets_prefix(self, tmp_path: Path) -> None:
|
||||
"""Root README should use 'assets/' prefix."""
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
resolved = _resolve_selector(html, tmp_path / "README.md", tmp_path)
|
||||
assert 'src="assets/' in resolved
|
||||
assert 'src="../assets/' not in resolved
|
||||
|
||||
def test_alternatives_readme_uses_parent_assets_prefix(self, tmp_path: Path) -> None:
|
||||
"""README in alternatives should use '../assets/' prefix."""
|
||||
html = generate_style_selector(
|
||||
"classic",
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
resolved = _resolve_selector(
|
||||
html,
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
assert 'src="../assets/' in resolved
|
||||
|
||||
def test_root_readme_links_to_alternatives_with_full_path(self, tmp_path: Path) -> None:
|
||||
"""Root README should link to alternatives with full path."""
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
assert "README_ALTERNATIVES/" in html
|
||||
|
||||
def test_selector_uses_asset_tokens(self, tmp_path: Path) -> None:
|
||||
"""Style selector should emit asset tokens, not concrete prefixes."""
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
assert "ASSET_PATH" in html
|
||||
assert "assets/" not in html
|
||||
|
||||
def test_alternatives_readme_links_to_root_with_parent(self, tmp_path: Path) -> None:
|
||||
"""Alternatives README should link to root with '../'."""
|
||||
html = generate_style_selector(
|
||||
"classic",
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
assert 'href="../"' in html
|
||||
|
||||
def test_alternatives_readme_links_to_siblings_with_filename(self, tmp_path: Path) -> None:
|
||||
"""Alternatives README should link to siblings with just filename."""
|
||||
html = generate_style_selector(
|
||||
"classic",
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
assert '.md"' in html
|
||||
|
||||
def test_current_style_gets_highlight_border(self, tmp_path: Path) -> None:
|
||||
"""Current style should have a highlight border."""
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
assert "border:" in html
|
||||
|
||||
def test_selector_includes_all_styles_in_order(self, tmp_path: Path) -> None:
|
||||
"""Selector should include all styles in configured order."""
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
assert "badge-style-extra.svg" in html
|
||||
assert "badge-style-classic.svg" in html
|
||||
assert "badge-style-flat.svg" in html
|
||||
assert "badge-style-awesome.svg" in html
|
||||
|
||||
|
||||
class TestPathConsistency:
|
||||
"""Tests for path consistency across different scenarios."""
|
||||
|
||||
def test_asset_prefix_consistency(self, tmp_path: Path) -> None:
|
||||
"""Asset prefix should be consistent based on output path."""
|
||||
root_html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
alt_html = generate_style_selector(
|
||||
"extra",
|
||||
tmp_path / "README_ALTERNATIVES" / "README_EXTRA.md",
|
||||
tmp_path,
|
||||
)
|
||||
root_resolved = _resolve_selector(root_html, tmp_path / "README.md", tmp_path)
|
||||
alt_resolved = _resolve_selector(
|
||||
alt_html,
|
||||
tmp_path / "README_ALTERNATIVES" / "README_EXTRA.md",
|
||||
tmp_path,
|
||||
)
|
||||
|
||||
# Root uses assets/
|
||||
assert "assets/" in root_resolved
|
||||
# Alternatives uses ../assets/
|
||||
assert "../assets/" in alt_resolved
|
||||
|
||||
# Root should NOT use ../assets/
|
||||
assert "../assets/" not in root_resolved
|
||||
|
||||
def test_cross_linking_symmetry(self, tmp_path: Path) -> None:
|
||||
"""Links should be symmetric - root to alt and alt to root."""
|
||||
root_html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
alt_html = generate_style_selector(
|
||||
"classic",
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
|
||||
# Root links to alternatives with full path
|
||||
assert "README_ALTERNATIVES/" in root_html
|
||||
|
||||
# Alternatives links back to root with ../
|
||||
assert 'href="../"' in alt_html
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Tests for edge cases and error handling."""
|
||||
|
||||
def test_missing_style_config_handled(self, monkeypatch, tmp_path: Path) -> None:
|
||||
"""Should handle missing style configuration gracefully."""
|
||||
monkeypatch.setitem(readme_config.CONFIG, "readme", {"root_style": "nonexistent"})
|
||||
monkeypatch.setitem(readme_config.CONFIG, "styles", {})
|
||||
monkeypatch.setitem(readme_config.CONFIG, "style_order", [])
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
assert isinstance(html, str)
|
||||
|
||||
def test_unknown_style_id(self, tmp_path: Path) -> None:
|
||||
"""Should handle unknown style ID gracefully."""
|
||||
html = generate_style_selector("unknown_style", tmp_path / "README.md", tmp_path)
|
||||
assert isinstance(html, str)
|
||||
|
||||
|
||||
class TestAssumptionsDocumented:
|
||||
"""
|
||||
Meta-tests that document the key assumptions.
|
||||
|
||||
These tests serve as executable documentation of the assumptions
|
||||
the path-handling system relies on.
|
||||
"""
|
||||
|
||||
def test_assumption_readme_at_root_uses_assets_directly(self, tmp_path: Path) -> None:
|
||||
"""
|
||||
ASSUMPTION: A README at repo root resolves assets via 'assets/'.
|
||||
|
||||
This assumes the assets folder is a direct child of the repo root.
|
||||
"""
|
||||
html = generate_style_selector("extra", tmp_path / "README.md", tmp_path)
|
||||
resolved = _resolve_selector(html, tmp_path / "README.md", tmp_path)
|
||||
assert "../assets/" not in resolved
|
||||
assert "assets/" in resolved
|
||||
|
||||
def test_assumption_alternatives_one_level_deep(self, tmp_path: Path) -> None:
|
||||
"""
|
||||
ASSUMPTION: README_ALTERNATIVES/ is exactly one level below root.
|
||||
|
||||
This is why we resolve to '../assets/' for files in that folder.
|
||||
If README_ALTERNATIVES were nested deeper, paths would change.
|
||||
"""
|
||||
html = generate_style_selector(
|
||||
"classic",
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
resolved = _resolve_selector(
|
||||
html,
|
||||
tmp_path / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
tmp_path,
|
||||
)
|
||||
assert "../assets/" in resolved
|
||||
assert "../../" not in resolved
|
||||
|
||||
def test_assumption_root_style_is_root_readme(self) -> None:
|
||||
"""
|
||||
ASSUMPTION: The root_style in config determines which README is at root.
|
||||
|
||||
Changing root_style should move a different README to root.
|
||||
"""
|
||||
root_style = get_root_style()
|
||||
path = get_style_selector_target(root_style)
|
||||
assert path == "README.md"
|
||||
|
||||
def test_assumption_only_one_readme_at_root(self) -> None:
|
||||
"""
|
||||
ASSUMPTION: Exactly one style lives at README.md (the root style).
|
||||
|
||||
All other styles go to README_ALTERNATIVES/.
|
||||
"""
|
||||
styles = ["extra", "classic", "awesome", "flat"]
|
||||
root_count = sum(1 for s in styles if get_style_selector_target(s) == "README.md")
|
||||
assert root_count == 1, "Exactly one style should be at root"
|
||||
|
||||
def test_assumption_flat_is_special_case(self) -> None:
|
||||
"""
|
||||
ASSUMPTION: Flat style has many files, linked via README_FLAT_ALL_AZ.md.
|
||||
|
||||
The style selector links to this specific file as the entry point.
|
||||
"""
|
||||
styles = CONFIG.get("styles", {})
|
||||
flat_config = styles.get("flat", {})
|
||||
filename = flat_config.get("filename", "")
|
||||
assert "FLAT" in filename.upper()
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Integration tests for TOC anchor validation against GitHub HTML.
|
||||
|
||||
These tests validate that our generated TOC anchors match what GitHub
|
||||
actually produces when rendering the markdown. This catches anchor
|
||||
generation bugs that would result in broken TOC links.
|
||||
|
||||
HTML fixtures are stored in tests/fixtures/github-html/ and version controlled.
|
||||
To update fixtures:
|
||||
1. Push changes to GitHub
|
||||
2. Navigate to the README on GitHub
|
||||
3. Open browser dev tools (F12)
|
||||
4. Find the <article> element containing the README
|
||||
5. Copy inner HTML to the appropriate fixture file
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.testing.validate_toc_anchors import (
|
||||
compare_anchors,
|
||||
extract_github_anchor_ids,
|
||||
extract_toc_anchors_from_readme,
|
||||
normalize_anchor,
|
||||
)
|
||||
from scripts.utils.repo_root import find_repo_root
|
||||
|
||||
REPO_ROOT = find_repo_root(Path(__file__))
|
||||
FIXTURES_DIR = REPO_ROOT / "tests" / "fixtures" / "github-html"
|
||||
EXPECTED_ANCHORS_PATH = REPO_ROOT / "tests" / "fixtures" / "expected_toc_anchors.txt"
|
||||
|
||||
# Style configurations: (html_fixture, readme_path)
|
||||
# HTML fixture names indicate root vs non-root placement on GitHub
|
||||
STYLE_CONFIGS = {
|
||||
"awesome": (FIXTURES_DIR / "awesome-root.html", REPO_ROOT / "README.md"),
|
||||
"classic": (
|
||||
FIXTURES_DIR / "classic-non-root.html",
|
||||
REPO_ROOT / "README_ALTERNATIVES" / "README_CLASSIC.md",
|
||||
),
|
||||
"extra": (
|
||||
FIXTURES_DIR / "extra-non-root.html",
|
||||
REPO_ROOT / "README_ALTERNATIVES" / "README_EXTRA.md",
|
||||
),
|
||||
"flat": (
|
||||
FIXTURES_DIR / "flat-non-root.html",
|
||||
REPO_ROOT / "README_ALTERNATIVES" / "README_FLAT_ALL_AZ.md",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def is_placeholder(path: Path) -> bool:
|
||||
"""Check if an HTML fixture is a placeholder (not yet populated)."""
|
||||
if not path.exists():
|
||||
return True
|
||||
content = path.read_text(encoding="utf-8")
|
||||
return content.strip().startswith("<!-- PLACEHOLDER:")
|
||||
|
||||
|
||||
class TestAnchorExtraction:
|
||||
"""Unit tests for anchor extraction functions."""
|
||||
|
||||
def test_extract_github_anchors_finds_user_content_ids(self) -> None:
|
||||
html = """
|
||||
<h2 id="user-content-agent-skills-">Agent Skills</h2>
|
||||
<h3 id="user-content-general">General</h3>
|
||||
<div id="other-id">Not a heading</div>
|
||||
"""
|
||||
anchors = extract_github_anchor_ids(html)
|
||||
assert anchors == {"agent-skills-", "general"}
|
||||
|
||||
def test_extract_toc_anchors_markdown_style(self) -> None:
|
||||
readme = """
|
||||
- [Agent Skills](#agent-skills-)
|
||||
- [General](#general)
|
||||
"""
|
||||
anchors = extract_toc_anchors_from_readme(readme)
|
||||
assert "agent-skills-" in anchors
|
||||
assert "general" in anchors
|
||||
|
||||
def test_extract_toc_anchors_html_style(self) -> None:
|
||||
readme = """
|
||||
<a href="#agent-skills-">Agent Skills</a>
|
||||
<a href="#general">General</a>
|
||||
"""
|
||||
anchors = extract_toc_anchors_from_readme(readme)
|
||||
assert "agent-skills-" in anchors
|
||||
assert "general" in anchors
|
||||
|
||||
def test_extract_toc_anchors_excludes_back_to_top(self) -> None:
|
||||
readme = """
|
||||
- [Agent Skills](#agent-skills-)
|
||||
[🔝](#awesome-claude-code)
|
||||
"""
|
||||
anchors = extract_toc_anchors_from_readme(readme)
|
||||
assert "agent-skills-" in anchors
|
||||
assert "awesome-claude-code" not in anchors
|
||||
|
||||
def test_normalize_anchor_url_decodes(self) -> None:
|
||||
assert normalize_anchor("official-documentation-%EF%B8%8F") == "official-documentation-️"
|
||||
assert normalize_anchor("simple-anchor") == "simple-anchor"
|
||||
|
||||
|
||||
class TestAnchorComparison:
|
||||
"""Unit tests for anchor comparison logic."""
|
||||
|
||||
def test_compare_anchors_perfect_match(self) -> None:
|
||||
github = {"a", "b", "c"}
|
||||
toc = {"a", "b", "c"}
|
||||
matched, missing, extra = compare_anchors(github, toc)
|
||||
assert matched == {"a", "b", "c"}
|
||||
assert missing == set()
|
||||
assert extra == set()
|
||||
|
||||
def test_compare_anchors_with_url_encoded(self) -> None:
|
||||
github = {"test-️"} # Actual emoji
|
||||
toc = {"test-%EF%B8%8F"} # URL encoded
|
||||
matched, missing, _ = compare_anchors(github, toc)
|
||||
assert "test-️" in matched
|
||||
assert missing == set()
|
||||
|
||||
def test_compare_anchors_missing_in_github(self) -> None:
|
||||
github = {"a", "b"}
|
||||
toc = {"a", "b", "c"}
|
||||
_, missing, _ = compare_anchors(github, toc)
|
||||
assert "c" in missing
|
||||
|
||||
def test_compare_anchors_extra_in_github(self) -> None:
|
||||
github = {"a", "b", "c"}
|
||||
toc = {"a", "b"}
|
||||
_, _, extra = compare_anchors(github, toc)
|
||||
assert "c" in extra
|
||||
|
||||
|
||||
def _validate_style(style_name: str) -> None:
|
||||
"""Common validation logic for a README style."""
|
||||
html_path, readme_path = STYLE_CONFIGS[style_name]
|
||||
|
||||
html_content = html_path.read_text(encoding="utf-8")
|
||||
readme_content = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
github_anchors = extract_github_anchor_ids(html_content)
|
||||
toc_anchors = extract_toc_anchors_from_readme(readme_content)
|
||||
|
||||
_, missing_in_github, _ = compare_anchors(github_anchors, toc_anchors)
|
||||
|
||||
assert not missing_in_github, (
|
||||
f"[{style_name.upper()}] TOC contains anchors not found in GitHub HTML (broken links): "
|
||||
f"{sorted(missing_in_github)}"
|
||||
)
|
||||
|
||||
|
||||
class TestAwesomeStyle:
|
||||
"""Integration tests for AWESOME style (root README.md)."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_placeholder(STYLE_CONFIGS["awesome"][0]),
|
||||
reason="AWESOME HTML fixture not populated",
|
||||
)
|
||||
def test_toc_anchors_match_github(self) -> None:
|
||||
"""Verify all AWESOME style TOC anchors exist in GitHub HTML."""
|
||||
_validate_style("awesome")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_placeholder(STYLE_CONFIGS["awesome"][0]),
|
||||
reason="AWESOME HTML fixture not populated",
|
||||
)
|
||||
def test_expected_anchor_count(self) -> None:
|
||||
"""Verify AWESOME anchor count is reasonable."""
|
||||
html_path = STYLE_CONFIGS["awesome"][0]
|
||||
html_content = html_path.read_text(encoding="utf-8")
|
||||
github_anchors = extract_github_anchor_ids(html_content)
|
||||
assert len(github_anchors) >= 30, (
|
||||
f"Expected at least 30 anchors, found {len(github_anchors)}"
|
||||
)
|
||||
|
||||
|
||||
class TestClassicStyle:
|
||||
"""Integration tests for CLASSIC style (README_CLASSIC.md)."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_placeholder(STYLE_CONFIGS["classic"][0]),
|
||||
reason="CLASSIC HTML fixture not populated",
|
||||
)
|
||||
def test_toc_anchors_match_github(self) -> None:
|
||||
"""Verify all CLASSIC style TOC anchors exist in GitHub HTML."""
|
||||
_validate_style("classic")
|
||||
|
||||
|
||||
class TestExtraStyle:
|
||||
"""Integration tests for EXTRA style (README_EXTRA.md)."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_placeholder(STYLE_CONFIGS["extra"][0]),
|
||||
reason="EXTRA HTML fixture not populated - see tests/fixtures/github-html/extra.html",
|
||||
)
|
||||
def test_toc_anchors_match_github(self) -> None:
|
||||
"""Verify all EXTRA style TOC anchors exist in GitHub HTML."""
|
||||
_validate_style("extra")
|
||||
|
||||
|
||||
class TestFlatStyle:
|
||||
"""Integration tests for FLAT style (README_FLAT_ALL_AZ.md)."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_placeholder(STYLE_CONFIGS["flat"][0]),
|
||||
reason="FLAT HTML fixture not populated - see tests/fixtures/github-html/flat.html",
|
||||
)
|
||||
def test_toc_anchors_match_github(self) -> None:
|
||||
"""Verify all FLAT style TOC anchors exist in GitHub HTML."""
|
||||
_validate_style("flat")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not EXPECTED_ANCHORS_PATH.exists(),
|
||||
reason="Expected anchors fixture not generated",
|
||||
)
|
||||
class TestExpectedAnchorsFixture:
|
||||
"""Tests against the expected anchors fixture file (AWESOME style baseline)."""
|
||||
|
||||
def test_github_structure_unchanged(self) -> None:
|
||||
"""Detect if GitHub's anchor generation changed.
|
||||
|
||||
If this fails, GitHub may have changed how they generate anchor IDs.
|
||||
Update the fixture with: python -m scripts.testing.validate_toc_anchors --generate-expected
|
||||
"""
|
||||
html_path = STYLE_CONFIGS["awesome"][0]
|
||||
if is_placeholder(html_path):
|
||||
pytest.skip("AWESOME HTML fixture not populated")
|
||||
|
||||
expected = set(EXPECTED_ANCHORS_PATH.read_text().strip().split("\n"))
|
||||
html_content = html_path.read_text(encoding="utf-8")
|
||||
actual = extract_github_anchor_ids(html_content)
|
||||
|
||||
assert actual == expected, (
|
||||
f"GitHub anchor structure changed. "
|
||||
f"New: {actual - expected}, Removed: {expected - actual}. "
|
||||
f"If intentional, regenerate fixture with: "
|
||||
f"python -m scripts.testing.validate_toc_anchors --generate-expected"
|
||||
)
|
||||
237
.agent/knowledge/awesome_claude/tests/test_validate_links.py
Normal file
237
.agent/knowledge/awesome_claude/tests/test_validate_links.py
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for validate_links helpers and URL validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import scripts.validation.validate_links as validate_links # noqa: E402
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
"""Minimal response stub for requests.head."""
|
||||
|
||||
def __init__(self, status_code: int, headers: dict[str, str] | None = None) -> None:
|
||||
self.status_code = status_code
|
||||
self.headers = headers or {}
|
||||
|
||||
|
||||
def test_parse_last_modified_date_variants() -> None:
|
||||
iso_value = "2024-06-01T12:34:56Z"
|
||||
parsed = validate_links.parse_last_modified_date(iso_value)
|
||||
assert parsed is not None
|
||||
assert parsed.tzinfo is not None
|
||||
assert parsed.year == 2024
|
||||
|
||||
fallback_value = "2024-06-01:12-34-56"
|
||||
fallback_parsed = validate_links.parse_last_modified_date(fallback_value)
|
||||
assert fallback_parsed is not None
|
||||
assert fallback_parsed.year == 2024
|
||||
|
||||
assert validate_links.parse_last_modified_date("") is None
|
||||
assert validate_links.parse_last_modified_date("not-a-date") is None
|
||||
|
||||
|
||||
def test_is_stale_threshold() -> None:
|
||||
now = datetime.now(UTC)
|
||||
fresh = now - timedelta(days=validate_links.STALE_DAYS - 1)
|
||||
stale = now - timedelta(days=validate_links.STALE_DAYS + 1)
|
||||
assert validate_links.is_stale(fresh) is False
|
||||
assert validate_links.is_stale(stale) is True
|
||||
|
||||
|
||||
def test_ensure_stale_column_adds_defaults() -> None:
|
||||
fieldnames, rows = validate_links.ensure_stale_column(["A"], [{"A": "1"}])
|
||||
assert validate_links.STALE_HEADER_NAME in fieldnames
|
||||
assert rows[0][validate_links.STALE_HEADER_NAME] == ""
|
||||
|
||||
|
||||
def test_apply_overrides_sets_and_locks_fields() -> None:
|
||||
row = {
|
||||
validate_links.ID_HEADER_NAME: "res-1",
|
||||
validate_links.ACTIVE_HEADER_NAME: "TRUE",
|
||||
validate_links.LICENSE_HEADER_NAME: "NOT_FOUND",
|
||||
validate_links.LAST_CHECKED_HEADER_NAME: "",
|
||||
validate_links.LAST_MODIFIED_HEADER_NAME: "",
|
||||
"Description": "Old description",
|
||||
}
|
||||
overrides = {
|
||||
"res-1": {
|
||||
"active": "FALSE",
|
||||
"license": "MIT",
|
||||
"last_checked": "2024-01-01:00-00-00",
|
||||
"last_modified": "2024-01-02:00-00-00",
|
||||
"description": "New description",
|
||||
"skip_validation": True,
|
||||
"notes": "ignored",
|
||||
"active_locked": True,
|
||||
}
|
||||
}
|
||||
|
||||
updated, locked_fields, skip_validation = validate_links.apply_overrides(row, overrides)
|
||||
assert skip_validation is True
|
||||
assert locked_fields == {
|
||||
"active",
|
||||
"license",
|
||||
"last_checked",
|
||||
"last_modified",
|
||||
"description",
|
||||
}
|
||||
assert updated[validate_links.ACTIVE_HEADER_NAME] == "FALSE"
|
||||
assert updated[validate_links.LICENSE_HEADER_NAME] == "MIT"
|
||||
assert updated[validate_links.LAST_CHECKED_HEADER_NAME] == "2024-01-01:00-00-00"
|
||||
assert updated[validate_links.LAST_MODIFIED_HEADER_NAME] == "2024-01-02:00-00-00"
|
||||
assert updated["Description"] == "New description"
|
||||
|
||||
|
||||
def test_header_int_parsing() -> None:
|
||||
assert validate_links._header_int({"X": 5}, "X") == 5
|
||||
assert validate_links._header_int({"X": "10"}, "X") == 10
|
||||
assert validate_links._header_int({"X": b"12"}, "X") == 12
|
||||
assert validate_links._header_int({"X": "bad"}, "X") == 0
|
||||
assert validate_links._header_int({}, "missing") == 0
|
||||
|
||||
|
||||
def test_get_committer_date_from_response_prefers_commit_info() -> None:
|
||||
data = [
|
||||
{
|
||||
"commit": {
|
||||
"committer": {"date": "2024-05-01T00:00:00Z"},
|
||||
"author": {"date": "2024-04-01T00:00:00Z"},
|
||||
}
|
||||
}
|
||||
]
|
||||
assert validate_links.get_committer_date_from_response(data) == "2024-05-01T00:00:00Z"
|
||||
|
||||
|
||||
def test_get_committer_date_from_response_falls_back_to_author() -> None:
|
||||
data = [{"commit": {"author": {"date": "2024-04-01T00:00:00Z"}}}]
|
||||
assert validate_links.get_committer_date_from_response(data) == "2024-04-01T00:00:00Z"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("url", "expected"),
|
||||
[
|
||||
("https://www.npmjs.com/package/left-pad", ("npm", "left-pad")),
|
||||
("https://pypi.org/project/requests/", ("pypi", "requests")),
|
||||
("https://crates.io/crates/serde", ("crates", "serde")),
|
||||
("https://formulae.brew.sh/formula/wget", ("homebrew", "wget")),
|
||||
("https://github.com/owner/repo", ("github-releases", "owner/repo")),
|
||||
("https://example.com", (None, None)),
|
||||
],
|
||||
)
|
||||
def test_detect_package_info(url: str, expected: tuple[str | None, str | None]) -> None:
|
||||
assert validate_links.detect_package_info(url) == expected
|
||||
|
||||
|
||||
def test_get_latest_release_info_for_github(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_latest_release(owner: str, repo: str):
|
||||
return "2024-01-01:00-00-00", "v1.0.0"
|
||||
|
||||
monkeypatch.setattr(validate_links, "get_github_latest_release", fake_latest_release)
|
||||
release_date, version, source = validate_links.get_latest_release_info(
|
||||
"https://github.com/owner/repo"
|
||||
)
|
||||
assert release_date == "2024-01-01:00-00-00"
|
||||
assert version == "v1.0.0"
|
||||
assert source == "github-releases"
|
||||
|
||||
release_date, version, source = validate_links.get_latest_release_info("https://example.com")
|
||||
assert (release_date, version, source) == (None, None, None)
|
||||
|
||||
|
||||
def test_validate_url_non_github_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_head(*args, **kwargs):
|
||||
return DummyResponse(200)
|
||||
|
||||
monkeypatch.setattr(validate_links.requests, "head", fake_head)
|
||||
|
||||
ok, status, license_info, last_modified = validate_links.validate_url("https://example.com")
|
||||
assert ok is True
|
||||
assert status == 200
|
||||
assert license_info is None
|
||||
assert last_modified is None
|
||||
|
||||
|
||||
def test_validate_url_non_github_client_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_head(*args, **kwargs):
|
||||
return DummyResponse(404)
|
||||
|
||||
monkeypatch.setattr(validate_links.requests, "head", fake_head)
|
||||
|
||||
ok, status, _, _ = validate_links.validate_url("https://example.com/missing")
|
||||
assert ok is False
|
||||
assert status == 404
|
||||
|
||||
|
||||
def test_validate_url_non_github_retries_on_server_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
responses = iter([DummyResponse(500), DummyResponse(200)])
|
||||
|
||||
def fake_head(*args, **kwargs):
|
||||
return next(responses)
|
||||
|
||||
monkeypatch.setattr(validate_links.requests, "head", fake_head)
|
||||
monkeypatch.setattr(validate_links.random, "uniform", lambda *_: 0)
|
||||
monkeypatch.setattr(validate_links.time, "sleep", lambda *_: None)
|
||||
|
||||
ok, status, _, _ = validate_links.validate_url("https://example.com", max_retries=2)
|
||||
assert ok is True
|
||||
assert status == 200
|
||||
|
||||
|
||||
def test_validate_url_github_file_enriches_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_request(api_url: str, params: dict[str, object] | None = None):
|
||||
return 200, {}, {"ok": True}
|
||||
|
||||
monkeypatch.setattr(validate_links, "github_request_json_paced", fake_request)
|
||||
monkeypatch.setattr(validate_links, "get_github_license", lambda *_: "MIT")
|
||||
monkeypatch.setattr(
|
||||
validate_links,
|
||||
"get_github_last_modified",
|
||||
lambda *_: "2024-05-01:00-00-00",
|
||||
)
|
||||
|
||||
ok, status, license_info, last_modified = validate_links.validate_url(
|
||||
"https://github.com/owner/repo/blob/main/README.md"
|
||||
)
|
||||
assert ok is True
|
||||
assert status == 200
|
||||
assert license_info == "MIT"
|
||||
assert last_modified == "2024-05-01:00-00-00"
|
||||
|
||||
|
||||
def test_validate_url_github_rate_limit_retry(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
reset_at = str(int(time.time()))
|
||||
responses = iter(
|
||||
[
|
||||
(403, {"X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset_at}, None),
|
||||
(200, {}, {"ok": True}),
|
||||
]
|
||||
)
|
||||
|
||||
def fake_request(api_url: str, params: dict[str, object] | None = None):
|
||||
return next(responses)
|
||||
|
||||
sleep_calls: list[float] = []
|
||||
|
||||
def fake_sleep(seconds: float) -> None:
|
||||
sleep_calls.append(seconds)
|
||||
|
||||
monkeypatch.setattr(validate_links, "github_request_json_paced", fake_request)
|
||||
monkeypatch.setattr(validate_links.time, "sleep", fake_sleep)
|
||||
monkeypatch.setattr(validate_links, "get_github_license", lambda *_: None)
|
||||
monkeypatch.setattr(validate_links, "get_github_last_modified", lambda *_: None)
|
||||
|
||||
ok, status, _, _ = validate_links.validate_url("https://github.com/owner/repo")
|
||||
assert ok is True
|
||||
assert status == 200
|
||||
assert len(sleep_calls) == 1
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for single resource validation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import scripts.validation.validate_single_resource as validate_single_resource # noqa: E402
|
||||
|
||||
|
||||
def test_validate_single_resource_missing_primary() -> None:
|
||||
ok, enriched, errors = validate_single_resource.validate_single_resource(primary_link="")
|
||||
assert ok is False
|
||||
assert "Primary link is required" in errors
|
||||
assert "active" not in enriched
|
||||
|
||||
|
||||
def test_validate_single_resource_primary_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_validate(url: str):
|
||||
return False, 500, None, None
|
||||
|
||||
monkeypatch.setattr(validate_single_resource, "validate_url", fake_validate)
|
||||
|
||||
ok, enriched, errors = validate_single_resource.validate_single_resource(
|
||||
primary_link="https://example.com",
|
||||
display_name="Example",
|
||||
category="Test",
|
||||
)
|
||||
|
||||
assert ok is False
|
||||
assert enriched["active"] == "FALSE"
|
||||
assert any("Primary URL validation failed" in err for err in errors)
|
||||
assert enriched["last_checked"]
|
||||
|
||||
|
||||
def test_validate_single_resource_success_with_secondary(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
def fake_validate(url: str):
|
||||
calls.append(url)
|
||||
if len(calls) == 1:
|
||||
return True, 200, "MIT", "2024-06-01:00-00-00"
|
||||
return True, 200, None, None
|
||||
|
||||
monkeypatch.setattr(validate_single_resource, "validate_url", fake_validate)
|
||||
|
||||
ok, enriched, errors = validate_single_resource.validate_single_resource(
|
||||
primary_link="https://example.com",
|
||||
secondary_link="https://example.com/secondary",
|
||||
display_name="Example",
|
||||
category="Test",
|
||||
)
|
||||
|
||||
assert ok is True
|
||||
assert errors == []
|
||||
assert enriched["license"] == "MIT"
|
||||
assert enriched["last_modified"] == "2024-06-01:00-00-00"
|
||||
assert enriched["active"] == "TRUE"
|
||||
assert enriched["last_checked"]
|
||||
assert calls == ["https://example.com", "https://example.com/secondary"]
|
||||
|
||||
|
||||
def test_validate_resource_from_dict_maps_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_validate_single_resource(**_kwargs):
|
||||
return (
|
||||
True,
|
||||
{
|
||||
"license": "Apache-2.0",
|
||||
"last_modified": "2024-01-01:00-00-00",
|
||||
"last_checked": "2024-01-02:00-00-00",
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
validate_single_resource,
|
||||
"validate_single_resource",
|
||||
fake_validate_single_resource,
|
||||
)
|
||||
|
||||
resource = {
|
||||
"primary_link": "https://example.com",
|
||||
"display_name": "Example",
|
||||
"category": "Test",
|
||||
"license": "NOT_FOUND",
|
||||
}
|
||||
|
||||
ok, updated, errors = validate_single_resource.validate_resource_from_dict(resource)
|
||||
assert ok is True
|
||||
assert errors == []
|
||||
assert updated["license"] == "Apache-2.0"
|
||||
assert updated["last_modified"] == "2024-01-01:00-00-00"
|
||||
assert updated["last_checked"] == "2024-01-02:00-00-00"
|
||||
Reference in New Issue
Block a user