Skip to content

Commit 3a3b4c0

Browse files
chaliyclaude
andauthored
feat: process remaining issues (#308, #310, #311, #312, #321, #327, #329, #331, #332, #333, #334) (#393)
## Summary Resolves multiple open issues in a single batch: - **#308**: Add spec tests for builtins (cmd-suggestions) - **#310**: Add unit tests for core infrastructure - **#311**: Add unit tests for complex builtins - **#312**: Test Python framework integration modules (langchain, deepagents, pydantic_ai) - **#321**: Test Python bindings resource limits and error conditions - **#327**: Fix clippy::unwrap_used — scoped allows to function level - **#329**: Add Python type checking (mypy) to CI - **#331**: Implement `bc` math builtin with expression parser - **#332**: Improve error messages for LLM self-correction (command suggestions) - **#333**: VFS snapshot/restore for multi-turn conversations - **#334**: Fix process substitution path collision (AtomicU64 counter) Deferred issues (commented with rationale): - **#313**: Interpreter refactor (>500 lines, needs sub-issues) - **#316**: Python/Monty features (blocked on upstream) ## Test plan - [x] `cargo fmt --check` passes - [x] `cargo clippy --all-targets --all-features -- -D warnings` passes - [x] `cargo test --all-features` passes (all 73 tests) - [x] `ruff check` and `ruff format --check` pass - [x] 20 unit tests for bc builtin - [x] 15 spec tests for bc - [x] 5 new process substitution spec tests - [x] 10 snapshot/restore integration tests - [x] 11 new Python binding tests - [x] 12 Python framework integration tests Closes #308, Closes #310, Closes #311, Closes #312, Closes #321, Closes #327, Closes #329, Closes #331, Closes #332, Closes #333, Closes #334 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5604d6a commit 3a3b4c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+5584
-29
lines changed

.github/workflows/python.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ jobs:
4545
- name: Ruff format
4646
run: uvx ruff format --check crates/bashkit-python
4747

48+
- uses: actions/setup-python@v6
49+
with:
50+
python-version: "3.12"
51+
52+
- name: Mypy type check
53+
run: |
54+
pip install mypy
55+
mypy crates/bashkit-python/bashkit/ --ignore-missing-imports
56+
4857
test:
4958
name: Test (Python ${{ matrix.python-version }})
5059
runs-on: ubuntu-latest

crates/bashkit-python/bashkit/_bashkit.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class BashTool:
4545
def system_prompt(self) -> str: ...
4646
def input_schema(self) -> str: ...
4747
def output_schema(self) -> str: ...
48+
def reset(self) -> None: ...
4849

4950
class ScriptedTool:
5051
"""Compose Python callbacks as bash builtins for multi-tool orchestration.

crates/bashkit-python/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,11 @@ select = ["E", "F", "W", "I", "UP"]
4747
[tool.ruff.lint.isort]
4848
known-first-party = ["bashkit"]
4949

50+
[tool.mypy]
51+
python_version = "3.9"
52+
warn_return_any = true
53+
warn_unused_configs = true
54+
ignore_missing_imports = true
55+
5056
[tool.pytest.ini_options]
5157
asyncio_mode = "auto"

crates/bashkit-python/tests/test_bashkit.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,129 @@ def test_scripted_tool_dozen_tools():
485485
assert r.exit_code == 0
486486
lines = r.stdout.strip().splitlines()
487487
assert lines == [f"result-{i}" for i in range(12)]
488+
489+
490+
# ===========================================================================
491+
# BashTool: Resource limit enforcement
492+
# ===========================================================================
493+
494+
495+
def test_max_loop_iterations_prevents_infinite_loop():
496+
"""max_loop_iterations stops infinite loops."""
497+
tool = BashTool(max_loop_iterations=10)
498+
r = tool.execute_sync("i=0; while true; do i=$((i+1)); done; echo $i")
499+
# Should stop before completing — either error or truncated output
500+
assert r.exit_code != 0 or int(r.stdout.strip() or "0") <= 100
501+
502+
503+
def test_max_commands_limits_execution():
504+
"""max_commands stops after N commands."""
505+
tool = BashTool(max_commands=5)
506+
r = tool.execute_sync("echo 1; echo 2; echo 3; echo 4; echo 5; echo 6; echo 7; echo 8; echo 9; echo 10")
507+
# Should stop before all 10 commands complete
508+
lines = [line for line in r.stdout.strip().splitlines() if line]
509+
assert len(lines) < 10 or r.exit_code != 0
510+
511+
512+
# ===========================================================================
513+
# BashTool: Error conditions
514+
# ===========================================================================
515+
516+
517+
def test_malformed_bash_syntax():
518+
"""Unclosed quotes produce an error."""
519+
tool = BashTool()
520+
r = tool.execute_sync('echo "unclosed')
521+
# Should fail with parse error
522+
assert r.exit_code != 0 or r.error is not None
523+
524+
525+
def test_nonexistent_command():
526+
"""Unknown commands return exit code 127."""
527+
tool = BashTool()
528+
r = tool.execute_sync("nonexistent_xyz_cmd_12345")
529+
assert r.exit_code == 127
530+
531+
532+
def test_large_output():
533+
"""Large output is handled without crash."""
534+
tool = BashTool()
535+
r = tool.execute_sync("for i in $(seq 1 1000); do echo line$i; done")
536+
assert r.exit_code == 0
537+
lines = r.stdout.strip().splitlines()
538+
assert len(lines) == 1000
539+
540+
541+
def test_empty_input():
542+
"""Empty script returns success."""
543+
tool = BashTool()
544+
r = tool.execute_sync("")
545+
assert r.exit_code == 0
546+
assert r.stdout == ""
547+
548+
549+
# ===========================================================================
550+
# ScriptedTool: Edge cases
551+
# ===========================================================================
552+
553+
554+
def test_scripted_tool_callback_runtime_error():
555+
"""RuntimeError in callback is caught."""
556+
tool = ScriptedTool("api")
557+
tool.add_tool(
558+
"fail",
559+
"Fails with RuntimeError",
560+
callback=lambda p, s=None: (_ for _ in ()).throw(RuntimeError("runtime fail")),
561+
)
562+
r = tool.execute_sync("fail")
563+
assert r.exit_code != 0
564+
assert "runtime fail" in r.stderr
565+
566+
567+
def test_scripted_tool_callback_type_error():
568+
"""TypeError in callback is caught."""
569+
tool = ScriptedTool("api")
570+
tool.add_tool(
571+
"bad",
572+
"Fails with TypeError",
573+
callback=lambda p, s=None: (_ for _ in ()).throw(TypeError("bad type")),
574+
)
575+
r = tool.execute_sync("bad")
576+
assert r.exit_code != 0
577+
578+
579+
def test_scripted_tool_large_callback_output():
580+
"""Callbacks returning large output work."""
581+
tool = ScriptedTool("api")
582+
tool.add_tool(
583+
"big",
584+
"Returns large output",
585+
callback=lambda p, s=None: "x" * 10000 + "\n",
586+
)
587+
r = tool.execute_sync("big")
588+
assert r.exit_code == 0
589+
assert len(r.stdout.strip()) == 10000
590+
591+
592+
def test_scripted_tool_callback_returns_empty():
593+
"""Callback returning empty string is ok."""
594+
tool = ScriptedTool("api")
595+
tool.add_tool(
596+
"empty",
597+
"Returns nothing",
598+
callback=lambda p, s=None: "",
599+
)
600+
r = tool.execute_sync("empty")
601+
assert r.exit_code == 0
602+
603+
604+
@pytest.mark.asyncio
605+
async def test_async_multiple_tools():
606+
"""Multiple async calls to different tools work."""
607+
tool = ScriptedTool("api")
608+
tool.add_tool("a", "Tool A", callback=lambda p, s=None: "A\n")
609+
tool.add_tool("b", "Tool B", callback=lambda p, s=None: "B\n")
610+
r = await tool.execute("a; b")
611+
assert r.exit_code == 0
612+
assert "A" in r.stdout
613+
assert "B" in r.stdout
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Tests for framework integration modules (langchain, deepagents, pydantic_ai).
2+
3+
These tests verify the integration modules work without the external frameworks
4+
by testing the import-guarding, factory functions, and mock behavior.
5+
"""
6+
7+
import pytest
8+
9+
from bashkit import ScriptedTool
10+
11+
# ===========================================================================
12+
# langchain.py tests
13+
# ===========================================================================
14+
15+
16+
def test_langchain_import():
17+
"""langchain module imports without langchain installed."""
18+
from bashkit import langchain # noqa: F401
19+
20+
21+
def test_langchain_create_bash_tool_without_langchain():
22+
"""create_bash_tool raises ImportError when langchain not installed."""
23+
from bashkit.langchain import LANGCHAIN_AVAILABLE, create_bash_tool
24+
25+
if not LANGCHAIN_AVAILABLE:
26+
with pytest.raises(ImportError, match="langchain-core"):
27+
create_bash_tool()
28+
29+
30+
def test_langchain_create_scripted_tool_without_langchain():
31+
"""create_scripted_tool raises ImportError when langchain not installed."""
32+
from bashkit.langchain import LANGCHAIN_AVAILABLE, create_scripted_tool
33+
34+
if not LANGCHAIN_AVAILABLE:
35+
st = ScriptedTool("api")
36+
st.add_tool("noop", "No-op", callback=lambda p, s=None: "ok\n")
37+
with pytest.raises(ImportError, match="langchain-core"):
38+
create_scripted_tool(st)
39+
40+
41+
def test_langchain_all_exports():
42+
"""langchain __all__ contains expected symbols."""
43+
from bashkit.langchain import __all__
44+
45+
assert "create_bash_tool" in __all__
46+
assert "create_scripted_tool" in __all__
47+
assert "BashkitTool" in __all__
48+
assert "BashToolInput" in __all__
49+
50+
51+
# ===========================================================================
52+
# deepagents.py tests
53+
# ===========================================================================
54+
55+
56+
def test_deepagents_import():
57+
"""deepagents module imports without deepagents installed."""
58+
from bashkit import deepagents # noqa: F401
59+
60+
61+
def test_deepagents_create_bash_middleware_without_deepagents():
62+
"""create_bash_middleware raises ImportError when deepagents not installed."""
63+
from bashkit.deepagents import DEEPAGENTS_AVAILABLE, create_bash_middleware
64+
65+
if not DEEPAGENTS_AVAILABLE:
66+
with pytest.raises(ImportError, match="deepagents"):
67+
create_bash_middleware()
68+
69+
70+
def test_deepagents_create_bashkit_backend_without_deepagents():
71+
"""create_bashkit_backend raises ImportError when deepagents not installed."""
72+
from bashkit.deepagents import DEEPAGENTS_AVAILABLE, create_bashkit_backend
73+
74+
if not DEEPAGENTS_AVAILABLE:
75+
with pytest.raises(ImportError, match="deepagents"):
76+
create_bashkit_backend()
77+
78+
79+
def test_deepagents_all_exports():
80+
"""deepagents __all__ contains expected symbols."""
81+
from bashkit.deepagents import __all__
82+
83+
assert "create_bash_middleware" in __all__
84+
assert "create_bashkit_backend" in __all__
85+
assert "BashkitMiddleware" in __all__
86+
assert "BashkitBackend" in __all__
87+
88+
89+
def test_deepagents_now_iso():
90+
"""_now_iso returns ISO format string."""
91+
from bashkit.deepagents import _now_iso
92+
93+
ts = _now_iso()
94+
assert isinstance(ts, str)
95+
assert "T" in ts # ISO format has T separator
96+
97+
98+
# ===========================================================================
99+
# pydantic_ai.py tests
100+
# ===========================================================================
101+
102+
103+
def test_pydantic_ai_import():
104+
"""pydantic_ai module imports without pydantic-ai installed."""
105+
from bashkit import pydantic_ai # noqa: F401
106+
107+
108+
def test_pydantic_ai_create_bash_tool_without_pydantic():
109+
"""create_bash_tool raises ImportError when pydantic-ai not installed."""
110+
from bashkit.pydantic_ai import PYDANTIC_AI_AVAILABLE
111+
from bashkit.pydantic_ai import create_bash_tool as create_pydantic_tool
112+
113+
if not PYDANTIC_AI_AVAILABLE:
114+
with pytest.raises(ImportError, match="pydantic-ai"):
115+
create_pydantic_tool()
116+
117+
118+
def test_pydantic_ai_all_exports():
119+
"""pydantic_ai __all__ contains expected symbols."""
120+
from bashkit.pydantic_ai import __all__
121+
122+
assert "create_bash_tool" in __all__

0 commit comments

Comments
 (0)