项目文件夹

文件
wehub-resource-sync 2cab53bc94
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Has been cancelled
Tests / Code Quality (Ruff & Mypy) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Has been cancelled
Tests / Tests (push) Has been cancelled
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Has been cancelled
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) Has been cancelled
Docker Publish / Test Docker Images (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / Serial / Integration / E2E Tests (push) Has been cancelled
Tests / MCP Server Tests (push) Has been cancelled
Test Vector Database Adaptors / Test chroma Adaptor (push) Has been cancelled
Test Vector Database Adaptors / Test faiss Adaptor (push) Has been cancelled
Test Vector Database Adaptors / Test qdrant Adaptor (push) Has been cancelled
Test Vector Database Adaptors / Test weaviate Adaptor (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:46:28 +08:00

480 行
17 KiB
Python

"""Tests for the shared workflow_runner utility.
Covers:
- run_workflows() with no workflow flags → (False, [])
- run_workflows() with a single named workflow
- WorkflowEngine loads bundled presets by name (integration)
- run_workflows() with multiple named workflows (chaining)
- run_workflows() with inline --enhance-stage flags
- run_workflows() with both named and inline workflows
- collect_workflow_vars() parsing
- Dry-run mode triggers sys.exit(0)
"""
import argparse
from unittest.mock import MagicMock, patch
import pytest
from skill_seekers.cli.workflow_runner import collect_workflow_vars, run_workflows
# ─────────────────────────── helpers ────────────────────────────────────────
def make_args(
enhance_workflow=None,
enhance_stage=None,
var=None,
workflow_dry_run=False,
):
"""Build a minimal argparse.Namespace for testing."""
return argparse.Namespace(
enhance_workflow=enhance_workflow,
enhance_stage=enhance_stage,
var=var,
workflow_dry_run=workflow_dry_run,
)
# ─────────────────────────── collect_workflow_vars ──────────────────────────
class TestCollectWorkflowVars:
def test_no_vars(self):
args = make_args()
assert collect_workflow_vars(args) == {}
def test_single_var(self):
args = make_args(var=["key=value"])
assert collect_workflow_vars(args) == {"key": "value"}
def test_multiple_vars(self):
args = make_args(var=["a=1", "b=2", "c=hello world"])
result = collect_workflow_vars(args)
assert result == {"a": "1", "b": "2", "c": "hello world"}
def test_var_with_equals_in_value(self):
args = make_args(var=["url=http://example.com/a=b"])
result = collect_workflow_vars(args)
assert result == {"url": "http://example.com/a=b"}
def test_extra_context_merged(self):
args = make_args(var=["user_key=abc"])
result = collect_workflow_vars(args, extra={"extra_key": "xyz"})
assert result == {"user_key": "abc", "extra_key": "xyz"}
def test_extra_context_overridden_by_var(self):
# --var takes precedence because extra is added first, then var overwrites
args = make_args(var=["key=from_var"])
result = collect_workflow_vars(args, extra={"key": "from_extra"})
# var keys should win
assert result["key"] == "from_var"
def test_invalid_var_skipped(self):
"""Entries without '=' are silently skipped."""
args = make_args(var=["no_equals_sign", "good=value"])
result = collect_workflow_vars(args)
assert result == {"good": "value"}
# ─────────────────────────── run_workflows ──────────────────────────────────
class TestRunWorkflowsNoFlags:
def test_returns_false_empty_when_no_flags(self):
args = make_args()
executed, names = run_workflows(args)
assert executed is False
assert names == []
def test_returns_false_when_empty_lists(self):
args = make_args(enhance_workflow=[], enhance_stage=[])
executed, names = run_workflows(args)
assert executed is False
assert names == []
class TestRunWorkflowsSingle:
"""Single --enhance-workflow flag."""
def test_single_workflow_executes(self):
args = make_args(enhance_workflow=["minimal"])
mock_engine = MagicMock()
mock_engine.workflow.name = "minimal"
mock_engine.workflow.description = "A minimal workflow"
mock_engine.workflow.stages = [MagicMock(), MagicMock()]
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
):
executed, names = run_workflows(args)
assert executed is True
assert names == ["minimal"]
mock_engine.run.assert_called_once()
def test_single_workflow_failed_load_skipped(self):
args = make_args(enhance_workflow=["nonexistent-workflow"])
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
side_effect=FileNotFoundError("not found"),
):
executed, names = run_workflows(args)
assert executed is False
assert names == []
def test_single_workflow_run_failure_continues(self):
args = make_args(enhance_workflow=["minimal"])
mock_engine = MagicMock()
mock_engine.workflow.name = "minimal"
mock_engine.workflow.description = "desc"
mock_engine.workflow.stages = []
mock_engine.run.side_effect = RuntimeError("AI call failed")
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
):
executed, names = run_workflows(args)
# Engine failed → not counted as executed
assert executed is False
assert names == []
class TestRunWorkflowsMultiple:
"""Multiple --enhance-workflow flags (chaining)."""
def test_two_workflows_both_execute(self):
args = make_args(enhance_workflow=["security-focus", "minimal"])
engines = []
for wf_name in ["security-focus", "minimal"]:
m = MagicMock()
m.workflow.name = wf_name
m.workflow.description = f"desc of {wf_name}"
m.workflow.stages = [MagicMock()]
engines.append(m)
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
side_effect=engines,
):
executed, names = run_workflows(args)
assert executed is True
assert names == ["security-focus", "minimal"]
for engine in engines:
engine.run.assert_called_once()
def test_three_workflows_in_order(self):
workflow_names = ["security-focus", "minimal", "api-documentation"]
args = make_args(enhance_workflow=workflow_names)
run_order = []
engines = []
for wf_name in workflow_names:
m = MagicMock()
m.workflow.name = wf_name
m.workflow.description = "desc"
m.workflow.stages = []
# Track call order
m.run.side_effect = lambda *_a, _n=wf_name, **_kw: run_order.append(_n)
engines.append(m)
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
side_effect=engines,
):
executed, names = run_workflows(args)
assert executed is True
assert names == workflow_names
assert run_order == workflow_names # Preserves order
def test_partial_failure_partial_success(self):
"""One workflow fails to load; the other should still run."""
args = make_args(enhance_workflow=["bad-workflow", "minimal"])
good_engine = MagicMock()
good_engine.workflow.name = "minimal"
good_engine.workflow.description = "desc"
good_engine.workflow.stages = []
def side_effect(name, **_kwargs):
if name == "bad-workflow":
raise FileNotFoundError("not found")
return good_engine
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
side_effect=side_effect,
):
executed, names = run_workflows(args)
assert executed is True
assert names == ["minimal"] # Only successful one
class TestRunWorkflowsInlineStages:
"""--enhance-stage flags (combined into one inline workflow)."""
def test_inline_stages_execute(self):
args = make_args(enhance_stage=["security:Check security", "cleanup:Remove boilerplate"])
mock_engine = MagicMock()
mock_engine.workflow.name = "inline_workflow"
mock_engine.workflow.stages = [MagicMock(), MagicMock()]
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
) as MockEngine:
executed, names = run_workflows(args)
assert executed is True
assert "inline_workflow" in names
mock_engine.run.assert_called_once()
# Verify inline workflow was built correctly
call_kwargs = MockEngine.call_args[1]
stages = call_kwargs["workflow_data"]["stages"]
assert len(stages) == 2
assert stages[0]["name"] == "security"
assert stages[0]["prompt"] == "Check security"
assert stages[1]["name"] == "cleanup"
assert stages[1]["prompt"] == "Remove boilerplate"
def test_inline_stage_without_colon(self):
"""Stage spec without ':' uses the whole string as both name and prompt."""
args = make_args(enhance_stage=["analyze everything"])
mock_engine = MagicMock()
mock_engine.workflow.stages = []
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
) as MockEngine:
run_workflows(args)
call_kwargs = MockEngine.call_args[1]
stage = call_kwargs["workflow_data"]["stages"][0]
assert stage["name"] == "stage_1"
assert stage["prompt"] == "analyze everything"
def test_inline_stages_from_config_execute(self):
"""Regression for RT-06/I2: inline stages declared in a CONFIG file reach
ctx.enhancement.stages, not argv. The inline engine must be built from the
resolved list — reading args.enhance_stage (None here) dropped config
stages silently (and raised TypeError on the None default)."""
args = make_args(enhance_workflow=None, enhance_stage=None)
mock_ctx = MagicMock()
mock_ctx.enhancement.workflows = []
mock_ctx.enhancement.stages = ["security:Check security", "cleanup:Tidy up"]
mock_engine = MagicMock()
mock_engine.workflow.name = "inline_workflow"
mock_engine.workflow.stages = [MagicMock(), MagicMock()]
with (
patch(
"skill_seekers.cli.execution_context.ExecutionContext.get",
return_value=mock_ctx,
),
patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
) as MockEngine,
):
executed, names = run_workflows(args)
assert executed is True
assert "inline_workflow" in names
mock_engine.run.assert_called_once()
# Built from the CONFIG stages, not argv.
stages = MockEngine.call_args[1]["workflow_data"]["stages"]
assert [s["name"] for s in stages] == ["security", "cleanup"]
class TestRunWorkflowsMixed:
"""Both --enhance-workflow and --enhance-stage provided."""
def test_named_then_inline(self):
args = make_args(
enhance_workflow=["security-focus"],
enhance_stage=["extra:Extra stage"],
)
named_engine = MagicMock()
named_engine.workflow.name = "security-focus"
named_engine.workflow.description = "desc"
named_engine.workflow.stages = []
inline_engine = MagicMock()
inline_engine.workflow.stages = []
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
side_effect=[named_engine, inline_engine],
):
executed, names = run_workflows(args)
assert executed is True
assert "security-focus" in names
assert "inline_workflow" in names
named_engine.run.assert_called_once()
inline_engine.run.assert_called_once()
class TestRunWorkflowsVariables:
def test_variables_passed_to_run(self):
args = make_args(
enhance_workflow=["minimal"],
var=["framework=django", "depth=comprehensive"],
)
mock_engine = MagicMock()
mock_engine.workflow.name = "minimal"
mock_engine.workflow.description = "desc"
mock_engine.workflow.stages = []
with patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
):
run_workflows(args, context={"extra": "ctx"})
call_kwargs = mock_engine.run.call_args[1]
ctx = call_kwargs["context"]
assert ctx["framework"] == "django"
assert ctx["depth"] == "comprehensive"
assert ctx["extra"] == "ctx"
class TestRunWorkflowsDryRun:
def test_dry_run_calls_preview_not_run(self):
args = make_args(
enhance_workflow=["minimal"],
workflow_dry_run=True,
)
mock_engine = MagicMock()
mock_engine.workflow.name = "minimal"
mock_engine.workflow.description = "desc"
mock_engine.workflow.stages = []
with (
patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
return_value=mock_engine,
),
pytest.raises(SystemExit) as exc,
):
run_workflows(args)
assert exc.value.code == 0
mock_engine.preview.assert_called_once()
mock_engine.run.assert_not_called()
def test_dry_run_multiple_workflows_all_previewed(self):
args = make_args(
enhance_workflow=["security-focus", "minimal"],
workflow_dry_run=True,
)
engines = []
for name in ["security-focus", "minimal"]:
m = MagicMock()
m.workflow.name = name
m.workflow.description = "desc"
m.workflow.stages = []
engines.append(m)
with (
patch(
"skill_seekers.cli.enhancement_workflow.WorkflowEngine",
side_effect=engines,
),
pytest.raises(SystemExit),
):
run_workflows(args)
for engine in engines:
engine.preview.assert_called_once()
engine.run.assert_not_called()
# ────────────────── bundled preset loading (integration) ─────────────────────
class TestBundledPresetsLoad:
"""Verify WorkflowEngine can load each bundled preset by name.
These are real integration tests – they actually read the YAML files
shipped inside the package via importlib.resources.
"""
BUNDLED_NAMES = [
"default",
"minimal",
"security-focus",
"architecture-comprehensive",
"api-documentation",
]
@pytest.mark.parametrize("preset_name", BUNDLED_NAMES)
def test_bundled_preset_loads(self, preset_name):
from skill_seekers.cli.enhancement_workflow import WorkflowEngine
engine = WorkflowEngine(preset_name)
wf = engine.workflow
assert wf.name, f"Workflow '{preset_name}' has no name"
assert isinstance(wf.stages, list), "stages must be a list"
assert len(wf.stages) > 0, f"Workflow '{preset_name}' has no stages"
@pytest.mark.parametrize("preset_name", BUNDLED_NAMES)
def test_bundled_preset_stages_have_required_fields(self, preset_name):
from skill_seekers.cli.enhancement_workflow import WorkflowEngine
engine = WorkflowEngine(preset_name)
for stage in engine.workflow.stages:
assert stage.name, f"Stage in '{preset_name}' has no name"
assert stage.type in ("builtin", "custom"), (
f"Stage '{stage.name}' in '{preset_name}' has unknown type '{stage.type}'"
)
def test_unknown_preset_raises_file_not_found(self):
from skill_seekers.cli.enhancement_workflow import WorkflowEngine
with pytest.raises(FileNotFoundError):
WorkflowEngine("completely-nonexistent-preset-xyz")
def test_list_bundled_workflows_returns_all(self):
from skill_seekers.cli.enhancement_workflow import list_bundled_workflows
names = list_bundled_workflows()
for expected in self.BUNDLED_NAMES:
assert expected in names, f"'{expected}' not in bundled workflows: {names}"
def test_list_user_workflows_empty_when_no_user_dir(self, tmp_path, monkeypatch):
"""list_user_workflows returns [] when ~/.config/skill-seekers/workflows/ does not exist."""
from skill_seekers.cli import enhancement_workflow as ew_mod
import pathlib
fake_home = tmp_path / "fake_home"
fake_home.mkdir()
monkeypatch.setenv("HOME", str(fake_home))
# Also patch Path.home() used inside the module
monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
names = ew_mod.list_user_workflows()
assert names == []