Files
wehub-resource-sync 2cab53bc94
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Waiting to run
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers MCP Server - 25 tools for AI assistants dockerfile:Dockerfile.mcp name:skill-seekers-mcp]) (push) Waiting to run
Docker Publish / Test Docker Images (push) Blocked by required conditions
Test Vector Database Adaptors / Test chroma Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test faiss Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test qdrant Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test weaviate Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Waiting to run
Tests / Code Quality (Ruff & Mypy) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Waiting to run
Tests / Tests (push) Blocked by required conditions
Tests / Serial / Integration / E2E Tests (push) Blocked by required conditions
Tests / MCP Server Tests (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 12:46:28 +08:00

386 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Tests for CLI Parser System
Tests the modular parser registration system.
"""
import argparse
import pytest
from skill_seekers.cli.parsers import (
PARSERS,
SubcommandParser,
get_parser_names,
register_parsers,
)
from skill_seekers.cli.parsers.package_parser import PackageParser
class TestParserRegistry:
"""Test parser registry functionality."""
def test_all_parsers_registered(self):
"""Test that all parsers are registered."""
assert len(PARSERS) == 19, f"Expected 19 parsers, got {len(PARSERS)}"
def test_get_parser_names(self):
"""Test getting list of parser names."""
names = get_parser_names()
assert len(names) == 19
assert "create" in names
assert "package" in names
assert "upload" in names
assert "config" in names
assert "workflows" in names
def test_all_parsers_are_subcommand_parsers(self):
"""Test that all parsers inherit from SubcommandParser."""
for parser in PARSERS:
assert isinstance(parser, SubcommandParser)
def test_all_parsers_have_required_properties(self):
"""Test that all parsers have name, help, description."""
for parser in PARSERS:
assert hasattr(parser, "name")
assert hasattr(parser, "help")
assert hasattr(parser, "description")
assert isinstance(parser.name, str)
assert isinstance(parser.help, str)
assert isinstance(parser.description, str)
assert len(parser.name) > 0
assert len(parser.help) > 0
def test_all_parsers_have_add_arguments_method(self):
"""Test that all parsers implement add_arguments."""
for parser in PARSERS:
assert hasattr(parser, "add_arguments")
assert callable(parser.add_arguments)
def test_no_duplicate_parser_names(self):
"""Test that all parser names are unique."""
names = [p.name for p in PARSERS]
assert len(names) == len(set(names)), "Duplicate parser names found!"
class TestParserCreation:
"""Test parser creation functionality."""
def test_package_parser_creates_subparser(self):
"""Test that PackageParser creates valid subparser."""
main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers()
package_parser = PackageParser()
subparser = package_parser.create_parser(subparsers)
assert subparser is not None
assert package_parser.name == "package"
def test_register_parsers_creates_all_subcommands(self):
"""Test that register_parsers creates all subcommands."""
main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers(dest="command")
# Register all parsers
register_parsers(subparsers)
# Test that existing commands can be parsed
test_commands = [
"config --show",
"package output/test/",
"upload test.zip",
"enhance output/test/",
"estimate test.json",
]
for cmd in test_commands:
args = main_parser.parse_args(cmd.split())
assert args.command is not None
class TestSpecificParsers:
"""Test specific parser implementations."""
def test_package_parser_arguments(self):
"""Test PackageParser has correct arguments."""
main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers(dest="command")
package_parser = PackageParser()
package_parser.create_parser(subparsers)
args = main_parser.parse_args(["package", "output/test/"])
assert args.command == "package"
assert args.skill_directory == "output/test/"
args = main_parser.parse_args(["package", "output/test/", "--target", "gemini"])
assert args.target == "gemini"
args = main_parser.parse_args(["package", "output/test/", "--target", "ibm-bob"])
assert args.target == "ibm-bob"
args = main_parser.parse_args(["package", "output/test/", "--no-open"])
assert args.no_open is True
class TestCurrentCommands:
"""Test current CLI commands after Grand Unification."""
def test_all_current_commands_registered(self):
"""Test that all current commands are registered."""
names = get_parser_names()
# Commands that survived the Grand Unification
# (individual scraper commands removed; use 'create' instead)
current_commands = [
"config",
"create",
"enhance",
"enhance-status",
"package",
"upload",
"estimate",
"extract-test-examples",
"install-agent",
"install",
"resume",
"stream",
"update",
"multilang",
"quality",
"doctor",
"workflows",
"sync-config",
]
for cmd in current_commands:
assert cmd in names, f"Command '{cmd}' not found in parser registry!"
def test_removed_scraper_commands_not_present(self):
"""Test that individual scraper commands were removed."""
names = get_parser_names()
removed_commands = [
"scrape",
"github",
"pdf",
"video",
"word",
"epub",
"jupyter",
"html",
"openapi",
"asciidoc",
"pptx",
"rss",
"manpage",
"confluence",
"notion",
"chat",
]
for cmd in removed_commands:
assert cmd not in names, f"Removed command '{cmd}' still in parser registry!"
def test_command_count_matches(self):
"""Test that we have exactly 19 commands."""
assert len(PARSERS) == 19
assert len(get_parser_names()) == 19
if __name__ == "__main__":
pytest.main([__file__, "-v"])
def _capture_module_parser(module_main):
"""Capture the parser a module main() builds, without running it.
Spies on ArgumentParser.parse_args: main(args=None) builds its parser and
calls parse_args(), at which point we grab the parser and abort.
"""
captured = {}
class _Sentinel(Exception):
pass
original_parse = argparse.ArgumentParser.parse_args
def spy_parse(self_parser, *_a, **_k):
captured["parser"] = self_parser
raise _Sentinel
argparse.ArgumentParser.parse_args = spy_parse
try:
module_main(args=None)
except _Sentinel:
pass
except SystemExit:
# Some mains exit before parsing when an optional dep is missing
# (e.g. install without the mcp extra).
pass
finally:
argparse.ArgumentParser.parse_args = original_parse
if "parser" not in captured:
pytest.skip("module main() exited before building its parser (optional dep missing)")
return captured["parser"]
class TestCentralModuleParserSync:
"""Central parsers must accept every flag the command modules define.
A flag defined in a module's main() parser but missing from the central
SubcommandParser is REJECTED by the unified CLI with 'unrecognized
arguments' before main() ever runs — backfill_parser_defaults can only
patch absent attributes, not rescue rejected flags. These were real
shipping bugs (estimate --unlimited, update --apply-update, ...).
"""
@pytest.fixture(scope="class")
def central_parser(self):
from skill_seekers.cli.main import create_parser
return create_parser()
@pytest.mark.parametrize(
"argv",
[
["estimate", "x.json", "--unlimited"],
["estimate", "x.json", "--timeout", "5"],
["update", "dir/", "--generate-package", "pkg.json"],
["update", "dir/", "--apply-update", "pkg.json"],
["quality", "dir/", "--output", "report.json"],
["stream", "big.md", "--streaming-overlap-chars", "100"],
["stream", "big.md", "--batch-size", "10", "--checkpoint", "c.json"],
["stream", "big.md", "--output", "out/"],
["multilang", "dir/", "--report"],
["multilang", "dir/", "--export", "out/"],
["multilang", "dir/", "--languages", "en", "es", "--detect"],
],
)
def test_previously_drifted_flags_parse(self, central_parser, argv):
args = central_parser.parse_args(argv)
assert args.command == argv[0]
def _module_parser(self, module_main):
"""Capture the parser a module main() builds, without running it."""
return _capture_module_parser(module_main)
@pytest.mark.parametrize(
"command,module_path",
[
("estimate", "skill_seekers.cli.estimate_pages"),
("update", "skill_seekers.cli.incremental_updater"),
("quality", "skill_seekers.cli.quality_metrics"),
("stream", "skill_seekers.cli.streaming_ingest"),
("multilang", "skill_seekers.cli.multilang_support"),
],
)
def test_no_module_flag_missing_from_central(self, central_parser, command, module_path):
"""Drift guard: every option dest in the module parser must exist in
the central subcommand parser (so the unified CLI accepts it)."""
import importlib
module = importlib.import_module(module_path)
module_parser = self._module_parser(module.main)
# Find the central subparser for this command
subparsers_action = next(
a for a in central_parser._actions if isinstance(a, argparse._SubParsersAction)
)
central_sub = subparsers_action.choices[command]
central_dests = {a.dest for a in central_sub._actions}
module_dests = {a.dest for a in module_parser._actions if a.dest != "help"}
missing = module_dests - central_dests
assert not missing, (
f"'{command}' module parser defines flags the central parser lacks "
f"(unified CLI will reject them): {sorted(missing)}"
)
# Commands migrated to single-definition parsers (Phase 5c): the module's
# main(args=None) standalone path builds its parser FROM the central
# SubcommandParser class, so flags exist in exactly one place.
SINGLE_SOURCE_COMMANDS = [
("config", "skill_seekers.cli.config_command"),
("enhance-status", "skill_seekers.cli.enhance_status"),
("upload", "skill_seekers.cli.upload_skill"),
("install", "skill_seekers.cli.install_skill"),
("install-agent", "skill_seekers.cli.install_agent"),
("estimate", "skill_seekers.cli.estimate_pages"),
("extract-test-examples", "skill_seekers.cli.test_example_extractor"),
("resume", "skill_seekers.cli.resume_command"),
("quality", "skill_seekers.cli.quality_metrics"),
("workflows", "skill_seekers.cli.workflows_command"),
("stream", "skill_seekers.cli.streaming_ingest"),
("update", "skill_seekers.cli.incremental_updater"),
("multilang", "skill_seekers.cli.multilang_support"),
]
class TestCentralParserSingleSource:
"""Phase 5c: each migrated command's flags are defined exactly once.
The module's standalone path (main(args=None)) must build a parser that
is INDISTINGUISHABLE from the central SubcommandParser — same option
dests AND same defaults. If someone re-adds a module-local add_argument
block (or the central parser changes without the module following,
which can't happen while the module builds FROM the central class),
this fails.
"""
@pytest.fixture(scope="class")
def central_parser(self):
from skill_seekers.cli.main import create_parser
return create_parser()
@staticmethod
def _dests_and_defaults(parser):
return {a.dest: a.default for a in parser._actions if a.dest != "help"}
@pytest.mark.parametrize(
"command,module_path", SINGLE_SOURCE_COMMANDS, ids=[c for c, _ in SINGLE_SOURCE_COMMANDS]
)
def test_module_parser_identical_to_central(self, central_parser, command, module_path):
import importlib
module = importlib.import_module(module_path)
module_parser = _capture_module_parser(module.main)
subparsers_action = next(
a for a in central_parser._actions if isinstance(a, argparse._SubParsersAction)
)
central_sub = subparsers_action.choices[command]
central = self._dests_and_defaults(central_sub)
module_map = self._dests_and_defaults(module_parser)
assert module_map == central, (
f"'{command}' module parser drifted from its central SubcommandParser.\n"
f" module-only/changed: "
f"{ {k: v for k, v in module_map.items() if central.get(k, object()) != v} }\n"
f" central-only/changed: "
f"{ {k: v for k, v in central.items() if module_map.get(k, object()) != v} }"
)
@pytest.mark.parametrize(
"command,module_path", SINGLE_SOURCE_COMMANDS, ids=[c for c, _ in SINGLE_SOURCE_COMMANDS]
)
def test_option_strings_identical_to_central(self, central_parser, command, module_path):
"""Flags (not just dests) must match — catches alias drift like -m/-u/-t."""
import importlib
module = importlib.import_module(module_path)
module_parser = _capture_module_parser(module.main)
subparsers_action = next(
a for a in central_parser._actions if isinstance(a, argparse._SubParsersAction)
)
central_sub = subparsers_action.choices[command]
def option_strings(parser):
return {tuple(a.option_strings) for a in parser._actions if a.dest != "help"}
assert option_strings(module_parser) == option_strings(central_sub)