feat: 优化运行时判定
This commit is contained in:
176
tests/test_cli.py
Normal file
176
tests/test_cli.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Tests for ``laya_router.cli`` PATH / symlink helpers.
|
||||
|
||||
These cover the path exposure logic added so ``laya-router`` and
|
||||
``laya-hook`` work from any shell without manual ``PATH`` setup.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from laya_router.cli import (
|
||||
_apply_path_symlinks,
|
||||
_bin_link_targets,
|
||||
_remove_path_symlinks,
|
||||
_user_path_bin_dir,
|
||||
ensure_path_symlinks,
|
||||
remove_path_symlinks,
|
||||
)
|
||||
|
||||
|
||||
class _FakeSettings:
|
||||
"""Minimal stand-in for ``Settings`` carrying just the paths we touch."""
|
||||
|
||||
def __init__(self, router: Path, hook: Path):
|
||||
self.router_bin = router
|
||||
self.hook_bin = hook
|
||||
self.mcp_bin = router.with_name("laya-mcp")
|
||||
|
||||
|
||||
def _make_fake_binaries(tmp_path: Path) -> tuple[Path, Path, _FakeSettings]:
|
||||
"""Build two venv-style binaries on disk and return (router, hook, settings)."""
|
||||
src_dir = tmp_path / "venv_bin"
|
||||
src_dir.mkdir()
|
||||
router = src_dir / "laya-router"
|
||||
hook = src_dir / "laya-hook"
|
||||
router.write_text("#!/bin/sh\necho router\n")
|
||||
hook.write_text("#!/bin/sh\necho hook\n")
|
||||
return router, hook, _FakeSettings(router, hook)
|
||||
|
||||
|
||||
def test_bin_link_targets_returns_both_binaries():
|
||||
from laya_router.config import load_settings
|
||||
|
||||
targets = _bin_link_targets(load_settings())
|
||||
names = [name for name, _ in targets]
|
||||
assert names == ["laya-router", "laya-hook", "laya-mcp"]
|
||||
assert targets[0][1].is_file()
|
||||
assert targets[1][1].is_file()
|
||||
|
||||
|
||||
def test_user_path_bin_dir_prefers_local_bin(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("PATH", f"{tmp_path}:/usr/bin")
|
||||
preferred = Path.home() / ".local" / "bin"
|
||||
if preferred.is_dir() and os.access(preferred, os.W_OK):
|
||||
assert _user_path_bin_dir() == preferred
|
||||
else:
|
||||
assert _user_path_bin_dir() == tmp_path
|
||||
|
||||
|
||||
def test_user_path_bin_dir_skips_system_dirs(monkeypatch):
|
||||
# Force "no writable dir exists" by overriding to a non-existent path.
|
||||
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", "/this/path/does/not/exist")
|
||||
monkeypatch.setenv("PATH", "/usr/local/bin:/usr/bin:/bin:/sbin")
|
||||
assert _user_path_bin_dir() is None
|
||||
|
||||
|
||||
def test_user_path_bin_dir_respects_env_override(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", str(tmp_path))
|
||||
assert _user_path_bin_dir() == tmp_path
|
||||
|
||||
|
||||
def test_user_path_bin_dir_env_override_rejects_unwritable(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", "/this/path/does/not/exist")
|
||||
assert _user_path_bin_dir() is None
|
||||
|
||||
|
||||
def test_apply_path_symlinks_creates_links(tmp_path):
|
||||
router, hook, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
|
||||
assert (bin_dir / "laya-router").is_symlink()
|
||||
assert (bin_dir / "laya-router").resolve() == router.resolve()
|
||||
assert (bin_dir / "laya-hook").is_symlink()
|
||||
assert (bin_dir / "laya-hook").resolve() == hook.resolve()
|
||||
|
||||
|
||||
def test_apply_path_symlinks_is_noop_when_correct(tmp_path):
|
||||
_, _, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
first = (bin_dir / "laya-router").lstat()
|
||||
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
second = (bin_dir / "laya-router").lstat()
|
||||
|
||||
# Re-running on a correct symlink must not rewrite it.
|
||||
assert first.st_mtime_ns == second.st_mtime_ns
|
||||
|
||||
|
||||
def test_apply_path_symlinks_repairs_wrong_target(tmp_path):
|
||||
router, _, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
(bin_dir / "laya-router").symlink_to("/etc/passwd")
|
||||
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
assert (bin_dir / "laya-router").resolve() == router.resolve()
|
||||
|
||||
|
||||
def test_apply_path_symlinks_repairs_broken_symlink(tmp_path):
|
||||
router, _, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
(bin_dir / "laya-router").symlink_to("/nonexistent/path")
|
||||
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
assert (bin_dir / "laya-router").resolve() == router.resolve()
|
||||
|
||||
|
||||
def test_apply_path_symlinks_skips_unrelated_file(tmp_path, capsys):
|
||||
router, _, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
existing = bin_dir / "laya-router"
|
||||
existing.write_text("user's binary")
|
||||
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "已被占用" in captured.err
|
||||
# The user's file must not be clobbered.
|
||||
assert existing.read_text() == "user's binary"
|
||||
# And the other one should still get a fresh symlink.
|
||||
assert (bin_dir / "laya-hook").is_symlink()
|
||||
assert (bin_dir / "laya-router").resolve() != router.resolve()
|
||||
|
||||
|
||||
def test_remove_path_symlinks_removes_both(tmp_path):
|
||||
_, _, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
_apply_path_symlinks(settings, bin_dir)
|
||||
assert (bin_dir / "laya-router").is_symlink()
|
||||
assert (bin_dir / "laya-hook").is_symlink()
|
||||
|
||||
_remove_path_symlinks(settings, bin_dir)
|
||||
assert not (bin_dir / "laya-router").exists()
|
||||
assert not (bin_dir / "laya-hook").exists()
|
||||
|
||||
|
||||
def test_remove_path_symlinks_idempotent(tmp_path):
|
||||
_, _, settings = _make_fake_binaries(tmp_path)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
|
||||
# No symlinks created — remove should still be safe.
|
||||
_remove_path_symlinks(settings, bin_dir)
|
||||
_remove_path_symlinks(settings, bin_dir)
|
||||
|
||||
|
||||
def test_public_helpers_warn_when_no_writable_path(monkeypatch, capsys):
|
||||
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", "/this/path/does/not/exist")
|
||||
settings = _FakeSettings(Path("/nonexistent/router"), Path("/nonexistent/hook"))
|
||||
|
||||
ensure_path_symlinks(settings)
|
||||
captured = capsys.readouterr()
|
||||
assert "没有可写的目录" in captured.err
|
||||
|
||||
# remove_path_symlinks is silent in the same situation.
|
||||
remove_path_symlinks(settings)
|
||||
@@ -43,6 +43,21 @@ def test_claude_merge_is_idempotent_and_keeps_existing_hooks(tmp_path):
|
||||
for item in group["hooks"]
|
||||
]
|
||||
assert commands == [f'"{tmp_path / "runtime" / ".venv" / "bin" / "laya-hook"}" --client claude']
|
||||
for event, matcher in (
|
||||
("PreToolUse", "Bash|Edit|Write|MultiEdit|apply_patch"),
|
||||
("PostToolUse", "Bash|Edit|Write|MultiEdit|apply_patch|Read|Grep|Glob"),
|
||||
):
|
||||
ours = [
|
||||
group
|
||||
for group in saved["hooks"][event]
|
||||
if any("laya-hook" in item.get("command", "") for item in group.get("hooks", []))
|
||||
]
|
||||
assert ours[-1]["matcher"] == matcher
|
||||
assert any(
|
||||
"laya-hook" in item.get("command", "")
|
||||
for group in saved["hooks"]["Stop"]
|
||||
for item in group["hooks"]
|
||||
)
|
||||
assert list(path.parent.glob("settings.json.bak-*"))
|
||||
|
||||
assert client.uninstall() is True
|
||||
|
||||
60
tests/test_evaluate.py
Normal file
60
tests/test_evaluate.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from laya_router.config import Settings
|
||||
from laya_router.evaluate import evaluate_event
|
||||
from laya_router.mcp import handle_message
|
||||
from laya_router.trajectory import SessionStore
|
||||
|
||||
|
||||
class _Agent:
|
||||
def predict(self, state, questions):
|
||||
answers = {}
|
||||
for name, spec in questions.items():
|
||||
if spec["type"] == "noul":
|
||||
value = 0.9 if name in {"is_stuck", "destructive", "claims_done_without_verification"} else 0.1
|
||||
answers[name] = {"noul": value}
|
||||
return {"answers": answers}
|
||||
|
||||
|
||||
def test_stop_blocks_only_once_per_turn(tmp_path):
|
||||
settings = Settings(home=tmp_path, skills=False)
|
||||
store = SessionStore()
|
||||
body = {
|
||||
"event": "PostToolUse",
|
||||
"session_id": "s",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {"file_path": "src/app.py"},
|
||||
"client": "claude",
|
||||
}
|
||||
evaluate_event(_Agent(), settings, store, body)
|
||||
stop = {
|
||||
"event": "Stop",
|
||||
"session_id": "s",
|
||||
"turn_id": "t1",
|
||||
"client": "codex",
|
||||
"stop_hook_active": False,
|
||||
}
|
||||
first = evaluate_event(_Agent(), settings, store, stop)
|
||||
assert first["action"] == "block"
|
||||
second = evaluate_event(_Agent(), settings, store, stop)
|
||||
assert second["action"] == "silent"
|
||||
|
||||
|
||||
def test_read_is_recorded_without_a_hint(tmp_path):
|
||||
settings = Settings(home=tmp_path)
|
||||
outcome = evaluate_event(
|
||||
_Agent(),
|
||||
settings,
|
||||
SessionStore(),
|
||||
{
|
||||
"event": "PostToolUse",
|
||||
"session_id": "s",
|
||||
"tool_name": "Read",
|
||||
"tool_input": {"file_path": "src/app.py"},
|
||||
},
|
||||
)
|
||||
assert outcome["action"] == "silent"
|
||||
|
||||
|
||||
def test_mcp_lists_the_judge_tool():
|
||||
listed = handle_message({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
|
||||
assert listed["result"]["tools"][0]["name"] == "laya_judge"
|
||||
assert handle_message({"jsonrpc": "2.0", "method": "notifications/initialized"}) is None
|
||||
@@ -79,6 +79,52 @@ def test_success_output(monkeypatch, tmp_path):
|
||||
assert "hint only" in context
|
||||
|
||||
|
||||
def test_pre_tool_deny_and_post_tool_context(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("LAYA_ROUTER_HOME", str(tmp_path))
|
||||
|
||||
def respond(settings, path, body, timeout):
|
||||
assert path == "/event"
|
||||
assert timeout == settings.event_timeout
|
||||
if body["event"] == "PreToolUse":
|
||||
return {"action": "deny", "reason": "do not delete", "context": ""}
|
||||
return {"action": "inject", "context": "replan", "reason": ""}
|
||||
|
||||
monkeypatch.setattr("laya_router.hook.post_json", respond)
|
||||
denied = json.loads(
|
||||
handle(
|
||||
{"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "rm -rf /"}},
|
||||
{},
|
||||
"claude",
|
||||
)
|
||||
)
|
||||
assert denied["hookSpecificOutput"]["permissionDecision"] == "deny"
|
||||
posted = json.loads(
|
||||
handle(
|
||||
{"hook_event_name": "PostToolUse", "tool_name": "Bash", "tool_input": {"command": "pytest"}},
|
||||
{},
|
||||
"codex",
|
||||
)
|
||||
)
|
||||
assert posted["hookSpecificOutput"]["hookEventName"] == "PostToolUse"
|
||||
assert posted["hookSpecificOutput"]["additionalContext"] == "replan"
|
||||
|
||||
|
||||
def test_stop_block_and_fail_open(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("LAYA_ROUTER_HOME", str(tmp_path))
|
||||
monkeypatch.setattr(
|
||||
"laya_router.hook.post_json",
|
||||
lambda settings, path, body, timeout: {"action": "block", "reason": "run pytest"},
|
||||
)
|
||||
blocked = json.loads(handle({"hook_event_name": "Stop", "stop_hook_active": False}, {}, "codex"))
|
||||
assert blocked == {"decision": "block", "reason": "run pytest"}
|
||||
|
||||
def boom(settings, path, body, timeout):
|
||||
raise URLError("connection refused")
|
||||
|
||||
monkeypatch.setattr("laya_router.hook.post_json", boom)
|
||||
assert handle({"hook_event_name": "Stop"}, {}, "claude") == "{}"
|
||||
|
||||
|
||||
def _payload():
|
||||
return {
|
||||
"level": "normal",
|
||||
|
||||
102
tests/test_policy.py
Normal file
102
tests/test_policy.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from laya_router.policy import decide_post_tool, decide_pre_tool, decide_stop
|
||||
|
||||
|
||||
def test_post_tool_dedupes_and_escalates_when_stuck():
|
||||
signals = {"consecutive_failures": 0, "repeated_command": 1}
|
||||
scores = {"is_stuck": 0.1, "should_replan": 0.1, "needs_more_context": 0.1}
|
||||
first = decide_post_tool(enabled=True, signals=signals, scores=scores, last_hint="")
|
||||
assert first.action == "inject"
|
||||
again = decide_post_tool(enabled=True, signals=signals, scores=scores, last_hint=first.context)
|
||||
assert again.action == "silent"
|
||||
stuck = decide_post_tool(
|
||||
enabled=True,
|
||||
signals={"consecutive_failures": 2, "repeated_command": 2},
|
||||
scores={"is_stuck": 0.9},
|
||||
last_hint="",
|
||||
)
|
||||
assert "replan" in stuck.context
|
||||
|
||||
|
||||
def test_gate_never_allows_and_codex_does_not_ask():
|
||||
quiet = decide_pre_tool(
|
||||
trajectory_enabled=True,
|
||||
gate_enabled=True,
|
||||
client="claude",
|
||||
tool_name="Bash",
|
||||
signals={"is_edit": False, "target_unread": False},
|
||||
edit_scores={},
|
||||
gate_scores={"destructive": 0.1, "irreversible": 0.1, "touches_secrets_or_prod": 0.1},
|
||||
)
|
||||
assert quiet.action == "silent"
|
||||
medium = decide_pre_tool(
|
||||
trajectory_enabled=False,
|
||||
gate_enabled=True,
|
||||
client="claude",
|
||||
tool_name="Bash",
|
||||
signals={},
|
||||
edit_scores={},
|
||||
gate_scores={"destructive": 0.6, "irreversible": 0.5, "touches_secrets_or_prod": 0.4},
|
||||
)
|
||||
assert medium.action == "ask"
|
||||
codex = decide_pre_tool(
|
||||
trajectory_enabled=False,
|
||||
gate_enabled=True,
|
||||
client="codex",
|
||||
tool_name="Bash",
|
||||
signals={},
|
||||
edit_scores={},
|
||||
gate_scores={"destructive": 0.6, "irreversible": 0.5, "touches_secrets_or_prod": 0.4},
|
||||
)
|
||||
assert codex.action == "inject"
|
||||
denied = decide_pre_tool(
|
||||
trajectory_enabled=False,
|
||||
gate_enabled=True,
|
||||
client="codex",
|
||||
tool_name="Bash",
|
||||
signals={},
|
||||
edit_scores={},
|
||||
gate_scores={"destructive": 0.95, "irreversible": 0.2, "touches_secrets_or_prod": 0.1},
|
||||
)
|
||||
assert denied.action == "deny"
|
||||
assert denied.action != "allow"
|
||||
|
||||
|
||||
def test_unread_edit_injects_hint():
|
||||
decision = decide_pre_tool(
|
||||
trajectory_enabled=True,
|
||||
gate_enabled=False,
|
||||
client="claude",
|
||||
tool_name="Edit",
|
||||
signals={"is_edit": True, "target_unread": True},
|
||||
edit_scores={"edit_premature": 0.2},
|
||||
gate_scores={},
|
||||
)
|
||||
assert decision.action == "inject"
|
||||
assert "read" in decision.context
|
||||
|
||||
|
||||
def test_stop_blocks_once_and_respects_the_loop_guard():
|
||||
scores = {"claims_done_without_verification": 0.8}
|
||||
signals = {"edited_without_verification": True}
|
||||
blocked = decide_stop(
|
||||
enabled=True,
|
||||
stop_hook_active=False,
|
||||
already_blocked=False,
|
||||
signals=signals,
|
||||
scores=scores,
|
||||
)
|
||||
assert blocked.action == "block"
|
||||
assert decide_stop(
|
||||
enabled=True,
|
||||
stop_hook_active=True,
|
||||
already_blocked=False,
|
||||
signals=signals,
|
||||
scores=scores,
|
||||
).action == "silent"
|
||||
assert decide_stop(
|
||||
enabled=True,
|
||||
stop_hook_active=False,
|
||||
already_blocked=True,
|
||||
signals=signals,
|
||||
scores=scores,
|
||||
).action == "silent"
|
||||
76
tests/test_skills.py
Normal file
76
tests/test_skills.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from laya_router.skills import decide_skill, discover_skills, suggest_skill
|
||||
|
||||
|
||||
def _skill(root, name, description, *, disabled=False, body="Do the documented steps."):
|
||||
folder = root / name
|
||||
folder.mkdir(parents=True)
|
||||
flag = "true" if disabled else "false"
|
||||
(folder / "SKILL.md").write_text(
|
||||
f"---\nname: {name}\ndescription: {description}\ndisable-model-invocation: {flag}\n---\n{body}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_discover_skips_disabled_skills(tmp_path):
|
||||
home = tmp_path / "home"
|
||||
_skill(home / ".claude" / "skills", "commit", "Create a git commit")
|
||||
_skill(home / ".agents" / "skills", "hidden", "Should stay hidden", disabled=True)
|
||||
found = discover_skills(None, home)
|
||||
assert [skill.name for skill in found] == ["commit"]
|
||||
|
||||
|
||||
def test_gate_and_fits_thresholds():
|
||||
assert decide_skill(
|
||||
ranked_names=["commit", "review"],
|
||||
gate_scores={"acts_on_user_system": 0.1, "would_follow_documented_procedure": 0.1, "prose_suffices": 0.9},
|
||||
rerank_choice="commit",
|
||||
fits=[0.9, 0.1],
|
||||
) is None
|
||||
assert decide_skill(
|
||||
ranked_names=["commit", "review"],
|
||||
gate_scores={"acts_on_user_system": 0.8, "would_follow_documented_procedure": 0.8, "prose_suffices": 0.1},
|
||||
rerank_choice="review",
|
||||
fits=[0.2, 0.1],
|
||||
) is None
|
||||
assert decide_skill(
|
||||
ranked_names=["commit", "review"],
|
||||
gate_scores={"acts_on_user_system": 0.8, "would_follow_documented_procedure": 0.8, "prose_suffices": 0.1},
|
||||
rerank_choice="review",
|
||||
fits=[0.4, 0.7],
|
||||
) == "review"
|
||||
|
||||
|
||||
class _Agent:
|
||||
def __init__(self, answers):
|
||||
self.answers = list(answers)
|
||||
self.calls = 0
|
||||
|
||||
def predict(self, state, questions):
|
||||
payload = self.answers[self.calls % len(self.answers)]
|
||||
self.calls += 1
|
||||
return {"answers": payload}
|
||||
|
||||
|
||||
def test_suggest_skips_a_skill_already_named(tmp_path):
|
||||
home = tmp_path / "home"
|
||||
_skill(home / ".claude" / "skills", "commit", "Create a git commit")
|
||||
skills = discover_skills(None, home)
|
||||
agent = _Agent(
|
||||
[
|
||||
{
|
||||
"which": {"choice": "commit", "probabilities": {"commit": 0.9}},
|
||||
"acts_on_user_system": {"noul": 0.9},
|
||||
"would_follow_documented_procedure": {"noul": 0.9},
|
||||
"prose_suffices": {"noul": 0.1},
|
||||
},
|
||||
{
|
||||
"which": {"choice": "commit"},
|
||||
"fits_0": {"noul": 0.8},
|
||||
},
|
||||
]
|
||||
)
|
||||
first = suggest_skill(agent, skills, "commit these changes", inject="suggest", already=set())
|
||||
assert first["name"] == "commit"
|
||||
assert first["mode"] == "suggest"
|
||||
again = suggest_skill(agent, skills, "commit these changes", inject="suggest", already={"commit"})
|
||||
assert again is None
|
||||
44
tests/test_trajectory.py
Normal file
44
tests/test_trajectory.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from laya_router.trajectory import (
|
||||
Step,
|
||||
consecutive_failures,
|
||||
edited_without_verification,
|
||||
repeated_command,
|
||||
response_failed,
|
||||
step_from_event,
|
||||
target_paths,
|
||||
was_inspected,
|
||||
)
|
||||
|
||||
|
||||
def test_patch_and_edit_paths():
|
||||
assert target_paths("Edit", {"file_path": "src/app.py"}) == ("src/app.py",)
|
||||
patch = "*** Update File: src/app.py\n*** Add File: src/new.py\n"
|
||||
assert target_paths("apply_patch", {"command": patch}) == ("src/app.py", "src/new.py")
|
||||
|
||||
|
||||
def test_failure_signals():
|
||||
assert response_failed({"exit_code": 1})
|
||||
assert not response_failed({"exit_code": 0})
|
||||
assert response_failed("command failed: boom")
|
||||
steps = [
|
||||
Step("bash", "Bash", "pytest", failed=True),
|
||||
Step("bash", "Bash", "pytest", failed=True),
|
||||
]
|
||||
assert consecutive_failures(steps) == 2
|
||||
assert repeated_command(steps) == 2
|
||||
steps.append(Step("bash", "Bash", "pytest", failed=False))
|
||||
assert consecutive_failures(steps) == 0
|
||||
|
||||
|
||||
def test_unread_edit_and_verification():
|
||||
read = step_from_event("Read", {"file_path": "src/app.py"}, None, after=True)
|
||||
edit = step_from_event("Edit", {"file_path": "src/app.py"}, None, after=True)
|
||||
other = step_from_event("Edit", {"file_path": "src/other.py"}, None, after=True)
|
||||
verify = step_from_event("Bash", {"command": "pytest -q"}, {"exit_code": 0}, after=True)
|
||||
assert read is not None and edit is not None and other is not None and verify is not None
|
||||
steps = [read, edit]
|
||||
assert was_inspected(steps, "src/app.py")
|
||||
assert not was_inspected(steps, "src/other.py")
|
||||
assert edited_without_verification([edit, other])
|
||||
assert not edited_without_verification([edit, verify])
|
||||
assert step_from_event("Edit", {"file_path": "a.py"}, None, after=False) is None
|
||||
Reference in New Issue
Block a user