Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions format.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
- Rewrite import statements across project files accordingly.
- Optional: refresh package API blocks after rename.

6) align-init-all-order
- Align top-level ``__all__`` order with import symbol order in ``__init__.py``.
- Remaining legacy names not found in imports are appended in original order.
- Supports both whole-project mode and single-file mode.

Examples:
python3 format.py sort-imports --root . --dry-run
python3 format.py sort-imports --root .
Expand All @@ -41,6 +46,8 @@
python3 format.py sync-imports-package-api --root . --dry-run
python3 format.py rename-private-modules --root . --dry-run
python3 format.py rename-private-modules --root . --build-package-api
python3 format.py align-init-all-order --root . --dry-run
python3 format.py align-init-all-order --path trpc_agent_sdk/skills/__init__.py
python3 format.py check-chinese --root . --dry-run
"""

Expand Down Expand Up @@ -1296,6 +1303,35 @@ def run_check_chinese(root: Path, dry_run: bool) -> int:
return 0


def run_align_init_all_order(root: Path, dry_run: bool, path: str | None) -> int:
"""Align __all__ order in __init__.py with import order."""
apply = not dry_run
target_files: list[Path] = []

if path:
target = Path(path).resolve()
if not target.exists() or not target.is_file():
print(f"Invalid --path: {target}", file=sys.stderr)
return 2
if target.name != "__init__.py":
print(f"--path must point to an __init__.py file: {target}", file=sys.stderr)
return 2
target_files = [target]
else:
target_files = [package_dir / "__init__.py" for package_dir in iter_package_dirs(root)]

changed_files: list[Path] = []
for init_path in sorted(set(target_files)):
if merge_init_all_exports(init_path, apply=apply):
changed_files.append(init_path)

mode = "DRY_RUN" if dry_run else "APPLY"
print(f"[{mode}] align-init-all-order scanned: {len(target_files)}, updated: {len(changed_files)}")
for p in changed_files:
print(str(p))
return 0


def main() -> int:
parser = argparse.ArgumentParser(description="Python import/public-API maintenance tool.")
subparsers = parser.add_subparsers(dest="command", required=True)
Expand Down Expand Up @@ -1349,6 +1385,14 @@ def main() -> int:
p_check_chinese.add_argument("--root", default=".", help="Project root directory.")
p_check_chinese.add_argument("--dry-run", action="store_true", help="Read-only mode (same output).")

p_align_all = subparsers.add_parser(
"align-init-all-order",
help="Align __all__ order with import order in __init__.py files.",
)
p_align_all.add_argument("--root", default=".", help="Project root directory.")
p_align_all.add_argument("--path", default=None, help="Optional path to a specific __init__.py file.")
p_align_all.add_argument("--dry-run", action="store_true", help="Preview changes without writing files.")

args = parser.parse_args()
try:
root = _validate_root(args.root)
Expand All @@ -1372,6 +1416,8 @@ def main() -> int:
return run_detect_issues(root, args.dry_run)
if args.command == "check-chinese":
return run_check_chinese(root, args.dry_run)
if args.command == "align-init-all-order":
return run_align_init_all_order(root, args.dry_run, args.path)

print(f"Unknown command: {args.command}", file=sys.stderr)
return 2
Expand Down
32 changes: 9 additions & 23 deletions tests/agents/core/test_skill_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@
_default_knowledge_only_guidance,
_default_full_tooling_and_workspace_guidance,
_default_tooling_and_workspace_guidance,
_is_knowledge_only,
_normalize_custom_guidance,
_normalize_load_mode,
_SKILLS_LOADED_ORDER_STATE_KEY,
_SKILLS_OVERVIEW_HEADER,
)
from trpc_agent_sdk.context import InvocationContext, create_agent_context
from trpc_agent_sdk.events import EventActions
Expand All @@ -38,6 +36,7 @@
SKILL_TOOLS_STATE_KEY_PREFIX,
BaseSkillRepository,
Skill,
SkillProfileFlags,
SkillResource,
SkillSummary,
)
Expand Down Expand Up @@ -169,20 +168,6 @@ def test_empty_mode_defaults_to_turn(self):
assert _normalize_load_mode(None) == SKILL_LOAD_MODE_TURN


class TestIsKnowledgeOnly:
def test_knowledge_only_profiles(self):
"""Recognized knowledge-only profile strings return True."""
assert _is_knowledge_only("knowledge_only") is True
assert _is_knowledge_only("knowledge") is True
assert _is_knowledge_only("Knowledge-Only") is True

def test_non_knowledge_profiles(self):
"""Non-matching profiles return False."""
assert _is_knowledge_only("full") is False
assert _is_knowledge_only("") is False
assert _is_knowledge_only(None) is False


class TestNormalizeCustomGuidance:
def test_empty_returns_empty(self):
"""Empty string passes through."""
Expand All @@ -207,27 +192,28 @@ def test_existing_newlines_preserved(self):
class TestGuidanceTextBuilders:
def test_knowledge_only_guidance_contains_header(self):
"""Knowledge-only guidance includes the tooling guidance header."""
text = _default_knowledge_only_guidance()
text = _default_knowledge_only_guidance(SkillProfileFlags.resolve_flags("knowledge_only"))
assert "Tooling and workspace guidance" in text

def test_full_tooling_guidance_exec_enabled(self):
"""Full tooling guidance with exec tools enabled mentions skill_exec."""
text = _default_full_tooling_and_workspace_guidance(exec_tools_disabled=False)
text = _default_full_tooling_and_workspace_guidance(SkillProfileFlags.resolve_flags("full"))
assert "skill_exec" in text

def test_full_tooling_guidance_exec_disabled(self):
"""Full tooling guidance with exec tools disabled omits skill_exec mention."""
text = _default_full_tooling_and_workspace_guidance(exec_tools_disabled=True)
text = _default_full_tooling_and_workspace_guidance(
SkillProfileFlags.resolve_flags("full").without_interactive_execution())
assert "interactive execution is available" in text

def test_default_routing_knowledge_only(self):
"""Dispatcher routes to knowledge-only guidance for matching profile."""
text = _default_tooling_and_workspace_guidance("knowledge_only", False)
text = _default_tooling_and_workspace_guidance(SkillProfileFlags.resolve_flags("knowledge_only"))
assert "progressive disclosure" in text

def test_default_routing_full(self):
"""Dispatcher routes to full guidance for non-matching profile."""
text = _default_tooling_and_workspace_guidance("", False)
text = _default_tooling_and_workspace_guidance(SkillProfileFlags.resolve_flags("full"))
assert "skill_run" in text


Expand Down Expand Up @@ -258,8 +244,8 @@ def test_custom_parameters(self, sample_repo):
assert proc._load_mode == SKILL_LOAD_MODE_ONCE
assert proc._tooling_guidance == "custom"
assert proc._tool_result_mode is True
assert proc._tool_profile == "knowledge_only"
assert proc._exec_tools_disabled is True
assert proc._tool_flags.load is True
assert proc._tool_flags.exec is False
assert proc._max_loaded_skills == 5


Expand Down
2 changes: 2 additions & 0 deletions tests/skills/test_run_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from trpc_agent_sdk.code_executors import WorkspaceRunResult
from trpc_agent_sdk.context import InvocationContext
from trpc_agent_sdk.skills import BaseSkillRepository
from trpc_agent_sdk.skills import Skill
from trpc_agent_sdk.skills.tools import ArtifactInfo
from trpc_agent_sdk.skills.tools import SkillRunFile
from trpc_agent_sdk.skills.tools import SkillRunInput
Expand Down Expand Up @@ -198,6 +199,7 @@ def setup_method(self):
self.mock_runtime.manager = Mock(return_value=self.mock_manager)
self.mock_runtime.fs = Mock(return_value=self.mock_fs)
self.mock_runtime.runner = Mock(return_value=self.mock_runner)
self.mock_repository.get = Mock(return_value=Skill(body="Command:\n echo hello\n"))

self.mock_ctx = Mock(spec=InvocationContext)
self.mock_ctx.agent_context = Mock()
Expand Down
12 changes: 6 additions & 6 deletions tests/skills/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ def test_skill_list_tools_success(self):

result = skill_list_tools(mock_ctx, "test-skill")

assert len(result["tools"]) == 3
assert "tool1" in result["tools"]
assert "tool2" in result["tools"]
assert "tool3" in result["tools"]
assert len(result["available_tools"]) == 3
assert "tool1" in result["available_tools"]
assert "tool2" in result["available_tools"]
assert "tool3" in result["available_tools"]

def test_skill_list_tools_no_tools(self):
"""Test listing tools for skill with no tools."""
Expand All @@ -158,7 +158,7 @@ def test_skill_list_tools_no_tools(self):

result = skill_list_tools(mock_ctx, "test-skill")

assert result["tools"] == []
assert result["available_tools"] == []

def test_skill_list_tools_skill_not_found(self):
"""Test listing tools for nonexistent skill."""
Expand All @@ -171,7 +171,7 @@ def test_skill_list_tools_skill_not_found(self):

result = skill_list_tools(mock_ctx, "nonexistent-skill")

assert result["tools"] == []
assert result["available_tools"] == []

def test_skill_list_tools_repository_not_found(self):
"""Test listing tools when repository not found."""
Expand Down
8 changes: 4 additions & 4 deletions tests/skills/tools/test_skill_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,26 +231,26 @@ async def test_put_and_get_session(self):
mock_session = MagicMock()
mock_session.exited_at = None
await tool._put_session("s1", mock_session)
result = await tool.get_session("s1")
result = await tool._get_session("s1")
assert result is mock_session

async def test_get_unknown_session_raises(self):
tool = self._make_exec_tool()
with pytest.raises(ValueError, match="unknown session_id"):
await tool.get_session("nonexistent")
await tool._get_session("nonexistent")

async def test_remove_session(self):
tool = self._make_exec_tool()
mock_session = MagicMock()
mock_session.exited_at = None
await tool._put_session("s1", mock_session)
result = await tool.remove_session("s1")
result = await tool._remove_session("s1")
assert result is mock_session

async def test_remove_unknown_session_raises(self):
tool = self._make_exec_tool()
with pytest.raises(ValueError, match="unknown session_id"):
await tool.remove_session("nonexistent")
await tool._remove_session("nonexistent")

def test_declaration(self):
tool = self._make_exec_tool()
Expand Down
86 changes: 5 additions & 81 deletions tests/skills/tools/test_skill_list_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Unit tests for trpc_agent_sdk.skills.tools._skill_list_tool.

Covers:
- _extract_shell_examples_from_skill_body: Command section parsing
- skill_list_tools: returns tools and command examples
- skill_list_tools: handles missing skill / repository
"""
"""Unit tests for trpc_agent_sdk.skills.tools._skill_list_tool."""

from __future__ import annotations

Expand All @@ -19,79 +13,10 @@

from trpc_agent_sdk.skills._types import Skill, SkillSummary
from trpc_agent_sdk.skills.tools._skill_list_tool import (
_extract_shell_examples_from_skill_body,
skill_list_tools,
)


# ---------------------------------------------------------------------------
# _extract_shell_examples_from_skill_body
# ---------------------------------------------------------------------------

class TestExtractShellExamples:
def test_empty_body(self):
assert _extract_shell_examples_from_skill_body("") == []

def test_command_section(self):
body = "Command:\n python scripts/run.py --input data.csv\n\nOverview"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) >= 1
assert "python scripts/run.py" in result[0]

def test_limit(self):
body = ""
for i in range(10):
body += f"Command:\n cmd_{i} --arg\n\n"
result = _extract_shell_examples_from_skill_body(body, limit=3)
assert len(result) <= 3

def test_stops_at_section_break(self):
body = "Command:\n python run.py\n\nOutput files\nMore content"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) == 1

def test_multiline_command(self):
body = "Command:\n python scripts/long.py \\\n --arg1 val1 \\\n --arg2 val2\n\n"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) >= 1

def test_no_command_section(self):
body = "# Overview\nJust a description.\n"
result = _extract_shell_examples_from_skill_body(body)
assert result == []

def test_deduplication(self):
body = "Command:\n python run.py\n\nCommand:\n python run.py\n"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) == 1

def test_rejects_non_command_starting_chars(self):
body = "Command:\n !not_a_command\n\n"
result = _extract_shell_examples_from_skill_body(body)
assert result == []

def test_command_with_numbered_break(self):
body = "Command:\n python run.py\n\n1) Next section\n"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) == 1

def test_skips_empty_lines_before_command(self):
body = "Command:\n\n\n python run.py\n\nEnd"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) >= 1

def test_stops_at_tools_section(self):
body = "Command:\n python run.py\n\ntools:\n- tool1"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) == 1

def test_whitespace_normalization(self):
body = "Command:\n python run.py --arg val\n\n"
result = _extract_shell_examples_from_skill_body(body)
assert len(result) == 1
assert " " not in result[0]


# ---------------------------------------------------------------------------
# skill_list_tools
# ---------------------------------------------------------------------------
Expand All @@ -103,7 +28,7 @@ def _make_ctx(repository=None):


class TestSkillListTools:
def test_returns_tools_and_examples(self):
def test_returns_tools(self):
skill = Skill(
summary=SkillSummary(name="test"),
body="Command:\n python run.py\n\nOverview",
Expand All @@ -114,16 +39,15 @@ def test_returns_tools_and_examples(self):
ctx = _make_ctx(repository=repo)

result = skill_list_tools(ctx, "test")
assert result["tools"] == ["get_weather", "get_data"]
assert len(result["command_examples"]) >= 1
assert result["available_tools"] == ["get_weather", "get_data"]

def test_skill_not_found(self):
repo = MagicMock()
repo.get = MagicMock(return_value=None)
ctx = _make_ctx(repository=repo)

result = skill_list_tools(ctx, "nonexistent")
assert result == {"tools": [], "command_examples": []}
assert result == {"available_tools": []}

def test_no_repository_raises(self):
ctx = _make_ctx(repository=None)
Expand All @@ -137,4 +61,4 @@ def test_no_tools_or_examples(self):
ctx = _make_ctx(repository=repo)

result = skill_list_tools(ctx, "test")
assert result["tools"] == []
assert result["available_tools"] == []
Loading
Loading