mirror of
https://github.com/shareAI-lab/learn-claude-code.git
synced 2026-08-26 16:52:26 +00:00
157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
import copy
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import types
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
LESSON = ROOT / "s11_background_tasks" / "code.py"
|
|
|
|
|
|
def load_lesson(workdir: Path):
|
|
fake_anthropic = types.ModuleType("anthropic")
|
|
|
|
class FakeAnthropic:
|
|
def __init__(self, *args, **kwargs):
|
|
self.messages = types.SimpleNamespace(create=None)
|
|
|
|
fake_dotenv = types.ModuleType("dotenv")
|
|
fake_anthropic.Anthropic = FakeAnthropic
|
|
fake_dotenv.load_dotenv = lambda override=True: None
|
|
|
|
previous_modules = {
|
|
"anthropic": sys.modules.get("anthropic"),
|
|
"dotenv": sys.modules.get("dotenv"),
|
|
}
|
|
previous_cwd = Path.cwd()
|
|
previous_model = os.environ.get("MODEL_ID")
|
|
module_name = f"background_tasks_test_{time.time_ns()}"
|
|
spec = importlib.util.spec_from_file_location(module_name, LESSON)
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
sys.modules["anthropic"] = fake_anthropic
|
|
sys.modules["dotenv"] = fake_dotenv
|
|
sys.modules[module_name] = module
|
|
try:
|
|
os.chdir(workdir)
|
|
os.environ["MODEL_ID"] = "test-model"
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
finally:
|
|
os.chdir(previous_cwd)
|
|
if previous_model is None:
|
|
os.environ.pop("MODEL_ID", None)
|
|
else:
|
|
os.environ["MODEL_ID"] = previous_model
|
|
for name, previous in previous_modules.items():
|
|
if previous is None:
|
|
sys.modules.pop(name, None)
|
|
else:
|
|
sys.modules[name] = previous
|
|
|
|
|
|
def wait_until(predicate, timeout: float = 2.0) -> bool:
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
if predicate():
|
|
return True
|
|
time.sleep(0.01)
|
|
return False
|
|
|
|
|
|
def test_s11_keeps_the_s04_kernel_and_adds_one_bash_option():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
lesson = load_lesson(Path(tmp))
|
|
|
|
assert {tool["name"] for tool in lesson.TOOLS} == {
|
|
"bash", "read_file", "write_file", "edit_file", "glob"
|
|
}
|
|
bash = next(tool for tool in lesson.TOOLS if tool["name"] == "bash")
|
|
assert "run_in_background" in bash["input_schema"]["properties"]
|
|
assert set(lesson.HOOKS) == {
|
|
"UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"
|
|
}
|
|
assert not hasattr(lesson, "Task")
|
|
assert not hasattr(lesson, "MEMORY_DIR")
|
|
|
|
|
|
def test_background_execution_requires_an_explicit_bash_flag():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
lesson = load_lesson(Path(tmp))
|
|
|
|
assert not lesson.should_run_background("bash", {"command": "npm install"})
|
|
assert lesson.should_run_background(
|
|
"bash", {"command": "printf ready", "run_in_background": True}
|
|
)
|
|
assert not lesson.should_run_background(
|
|
"write_file", {"run_in_background": True}
|
|
)
|
|
|
|
|
|
def test_background_bash_passes_permission_before_dispatch():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
lesson = load_lesson(Path(tmp))
|
|
block = types.SimpleNamespace(
|
|
id="tool_denied",
|
|
name="bash",
|
|
input={"command": "rm -rf /tmp/example", "run_in_background": True},
|
|
type="tool_use",
|
|
)
|
|
responses = [
|
|
types.SimpleNamespace(stop_reason="tool_use", content=[block]),
|
|
types.SimpleNamespace(
|
|
stop_reason="end_turn",
|
|
content=[types.SimpleNamespace(type="text", text="Denied.")],
|
|
),
|
|
]
|
|
lesson.client.messages.create = lambda **_: responses.pop(0)
|
|
history = [{"role": "user", "content": "Delete the directory"}]
|
|
|
|
lesson.agent_loop(history)
|
|
|
|
assert not lesson.background_tasks
|
|
result = history[2]["content"][0]
|
|
assert result["type"] == "tool_result"
|
|
assert "Permission denied" in result["content"]
|
|
|
|
|
|
def test_completed_result_is_collected_once_before_a_later_llm_call():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
lesson = load_lesson(Path(tmp))
|
|
block = types.SimpleNamespace(
|
|
id="tool_ready",
|
|
name="bash",
|
|
input={"command": "printf ready", "run_in_background": True},
|
|
)
|
|
task_id = lesson.start_background_task(block)
|
|
assert wait_until(
|
|
lambda: lesson.background_tasks[task_id]["status"] == "completed"
|
|
)
|
|
|
|
seen_messages = []
|
|
|
|
def respond(**kwargs):
|
|
seen_messages.append(copy.deepcopy(kwargs["messages"]))
|
|
return types.SimpleNamespace(
|
|
stop_reason="end_turn",
|
|
content=[types.SimpleNamespace(type="text", text="Received.")],
|
|
)
|
|
|
|
lesson.client.messages.create = respond
|
|
history = [{"role": "user", "content": "Continue"}]
|
|
lesson.agent_loop(history)
|
|
|
|
delivered = str(seen_messages[0])
|
|
assert "<task_notification>" in delivered
|
|
assert f"<task_id>{task_id}</task_id>" in delivered
|
|
assert "<status>completed</status>" in delivered
|
|
assert "ready" in delivered
|
|
assert lesson.collect_background_results() == []
|
|
|
|
|
|
def test_s11_code_is_ascii():
|
|
LESSON.read_text(encoding="ascii")
|