feat(agent): hierarchical executor with path resolve, runtime probe, quiet UI
Make Zed Agent closer to Cursor: deterministic DevOps path index, live Traefik port probe before blind edits, stop-after-edit, and quieter Russian progress.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
"""Unit tests for deterministic path index + resolve gating."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from path_resolve import (
|
||||
apply_deterministic_path_index,
|
||||
enrich_plan_from_discovery_tools,
|
||||
match_index_paths,
|
||||
path_resolve_needed,
|
||||
)
|
||||
|
||||
|
||||
class TestPathResolve(unittest.TestCase):
|
||||
def test_index_hits_traefik_not_fake_yml(self) -> None:
|
||||
plan = {
|
||||
"user_goal": "fix observer.ift.calentiq.com traefik route",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "1",
|
||||
"prompt": "edit traefik for observer",
|
||||
"paths": [
|
||||
r"C:\Users\alexc\IdeaProjects\eventHub\EventHubDevOps\ift\traefik\traefik.yml"
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
# sanitize would drop fake; index should still hit via keywords
|
||||
indexed = match_index_paths(plan)
|
||||
self.assertTrue(indexed)
|
||||
self.assertTrue(
|
||||
indexed[0].lower().replace("/", "\\").endswith(
|
||||
r"ift\traefik\dynamic_conf.yml"
|
||||
)
|
||||
)
|
||||
fixed, unresolved = apply_deterministic_path_index(plan)
|
||||
self.assertEqual(unresolved, [])
|
||||
self.assertFalse(path_resolve_needed(fixed))
|
||||
self.assertTrue(
|
||||
fixed["subtasks"][0]["paths"][0]
|
||||
.lower()
|
||||
.replace("/", "\\")
|
||||
.endswith(r"ift\traefik\dynamic_conf.yml")
|
||||
)
|
||||
|
||||
def test_index_compose_core(self) -> None:
|
||||
plan = {
|
||||
"user_goal": "bump timeout in docker compose swarm",
|
||||
"subtasks": [{"id": "1", "prompt": "edit compose", "paths": []}],
|
||||
}
|
||||
fixed, unresolved = apply_deterministic_path_index(plan)
|
||||
self.assertEqual(unresolved, [])
|
||||
self.assertIn("docker-compose.core.yml", fixed["subtasks"][0]["paths"][0])
|
||||
|
||||
def test_miss_triggers_queries(self) -> None:
|
||||
plan = {
|
||||
"user_goal": "EventHubDevOps mystery widget xyz",
|
||||
"subtasks": [{"id": "1", "prompt": "widget", "paths": []}],
|
||||
}
|
||||
fixed, unresolved = apply_deterministic_path_index(plan)
|
||||
self.assertTrue(unresolved)
|
||||
self.assertTrue(path_resolve_needed(fixed))
|
||||
|
||||
def test_enrich_from_find_tool(self) -> None:
|
||||
plan = {
|
||||
"path_resolve_queries": ["dynamic_conf.yml"],
|
||||
"path_resolve": "needed",
|
||||
"subtasks": [{"id": "1", "prompt": "x", "paths": []}],
|
||||
}
|
||||
yml = r"C:\Users\alexc\IdeaProjects\eventHub\EventHubDevOps\ift\traefik\dynamic_conf.yml"
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "f1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "find_path",
|
||||
"arguments": json.dumps({"query": "dynamic_conf"}),
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "f1", "content": yml + "\n"},
|
||||
]
|
||||
enriched = enrich_plan_from_discovery_tools(plan, messages)
|
||||
self.assertEqual(enriched["path_resolve"], "find")
|
||||
self.assertFalse(path_resolve_needed(enriched))
|
||||
self.assertEqual(enriched["subtasks"][0]["paths"][0], yml)
|
||||
|
||||
|
||||
def test_enrich_ignores_yaml_etc_artifacts(self) -> None:
|
||||
plan = {
|
||||
"path_resolve": "index",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "1",
|
||||
"prompt": "traefik",
|
||||
"paths": [
|
||||
r"C:\Users\alexc\IdeaProjects\eventHub\EventHubDevOps\ift\traefik\dynamic_conf.yml"
|
||||
],
|
||||
"path_source": "index",
|
||||
}
|
||||
],
|
||||
}
|
||||
# read_file body mentions container paths — must NOT poison plan
|
||||
yml_body = (
|
||||
"http:\n routers:\n"
|
||||
" # volume: /etc/traefik/dynamic_conf.yml\n"
|
||||
" # also null:/etc/nginx/conf.d/default.conf\n"
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "r1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": json.dumps(
|
||||
{
|
||||
"path": r"C:\Users\alexc\IdeaProjects\eventHub\EventHubDevOps\ift\traefik\dynamic_conf.yml"
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "r1", "content": yml_body},
|
||||
]
|
||||
enriched = enrich_plan_from_discovery_tools(plan, messages)
|
||||
self.assertEqual(enriched["path_resolve"], "index")
|
||||
self.assertIn(
|
||||
"dynamic_conf.yml",
|
||||
enriched["subtasks"][0]["paths"][0],
|
||||
)
|
||||
self.assertNotIn("nginx", json.dumps(enriched).lower())
|
||||
# Without index, still ignore read bodies (no find_* call ids)
|
||||
plan2 = {
|
||||
"path_resolve_queries": ["x"],
|
||||
"path_resolve": "needed",
|
||||
"subtasks": [{"id": "1", "prompt": "x", "paths": []}],
|
||||
}
|
||||
enriched2 = enrich_plan_from_discovery_tools(plan2, messages)
|
||||
self.assertEqual(enriched2.get("path_resolve"), "needed")
|
||||
self.assertEqual(enriched2["subtasks"][0]["paths"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Tests for Zed-facing progress UI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from progress_ui import (
|
||||
execution_banner,
|
||||
format_progress_block,
|
||||
humanize_line,
|
||||
short_path,
|
||||
tool_status,
|
||||
)
|
||||
|
||||
|
||||
class TestProgressUi(unittest.TestCase):
|
||||
def test_humanize_drops_noise(self) -> None:
|
||||
self.assertIsNone(humanize_line("ждём LiteLLM `b-complex` (до 45с)…"))
|
||||
self.assertIsNone(humanize_line("ctx 7358/12000"))
|
||||
self.assertEqual(humanize_line("план…"), "Планирую задачу")
|
||||
self.assertIn(
|
||||
"утверждён",
|
||||
humanize_line("план: утверждён пользователем (3 подзадач)") or "",
|
||||
)
|
||||
|
||||
def test_format_block_numbered(self) -> None:
|
||||
block = format_progress_block(
|
||||
[
|
||||
"план…",
|
||||
"ждём LiteLLM x",
|
||||
"план: утверждён пользователем (3 подзадач)",
|
||||
"agent: план готов → executor с tools (Zed)",
|
||||
]
|
||||
)
|
||||
self.assertIn("**Ход**", block)
|
||||
self.assertIn("1. Планирую задачу", block)
|
||||
self.assertIn("2. План утверждён", block)
|
||||
self.assertNotIn("LiteLLM", block)
|
||||
|
||||
def test_execution_banner_shows_short_path(self) -> None:
|
||||
plan = {
|
||||
"subtasks": [
|
||||
{
|
||||
"paths": [
|
||||
r"C:\Users\alexc\IdeaProjects\eventHub\EventHubDevOps\ift\traefik\dynamic_conf.yml"
|
||||
],
|
||||
"path_source": "index",
|
||||
}
|
||||
]
|
||||
}
|
||||
text = execution_banner(path_mode=False, plan=plan, model="a-medium-code")
|
||||
self.assertIn("**Выполнение**", text)
|
||||
self.assertIn("dynamic_conf.yml", text)
|
||||
self.assertIn("индекс", text)
|
||||
self.assertNotIn("a-medium-code", text)
|
||||
|
||||
def test_short_path_and_tool_status(self) -> None:
|
||||
self.assertEqual(
|
||||
short_path(r"C:\x\EventHubDevOps\ift\traefik\dynamic_conf.yml"),
|
||||
r"ift\traefik\dynamic_conf.yml",
|
||||
)
|
||||
self.assertIn("Читаю", tool_status(["read_file"], path=r"a\b\c.yml"))
|
||||
self.assertIn("Правлю", tool_status(["edit_file"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Unit tests for DevOps runtime_probe."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from runtime_probe import (
|
||||
attach_runtime_probe,
|
||||
devops_blocks_blind_force_edit,
|
||||
extract_hosts,
|
||||
is_devops_request,
|
||||
suggested_traefik_edit,
|
||||
)
|
||||
|
||||
|
||||
class TestRuntimeProbe(unittest.TestCase):
|
||||
def test_extract_hosts(self) -> None:
|
||||
hosts = extract_hosts(
|
||||
"observer.ift.calentiq.com не работает — Bad Gateway"
|
||||
)
|
||||
self.assertEqual(hosts, ["observer.ift.calentiq.com"])
|
||||
|
||||
def test_is_devops_request(self) -> None:
|
||||
self.assertTrue(
|
||||
is_devops_request(
|
||||
{"user_goal": "fix traefik"},
|
||||
"observer.ift.calentiq.com Bad Gateway",
|
||||
)
|
||||
)
|
||||
self.assertFalse(is_devops_request({"user_goal": "rename button"}))
|
||||
|
||||
def test_attach_probe_sets_facts(self) -> None:
|
||||
plan = {
|
||||
"user_goal": "observer.ift.calentiq.com Bad Gateway",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "1",
|
||||
"prompt": "fix traefik",
|
||||
"paths": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
fake = {
|
||||
"host": "observer.ift.calentiq.com",
|
||||
"http_status": 502,
|
||||
"service": "observer_web",
|
||||
"ports": {"observer_web:80": False, "observer_web:4000": True},
|
||||
"open_ports": [4000],
|
||||
"suggested_backend_url": "http://observer_web:4000",
|
||||
"hint": "port mismatch",
|
||||
"source": "gateway_tcp",
|
||||
"closed_port_80_but_alt_open": True,
|
||||
}
|
||||
with patch("runtime_probe.probe_host", return_value=fake):
|
||||
out = attach_runtime_probe(
|
||||
plan,
|
||||
user_text="observer.ift.calentiq.com не работает",
|
||||
cfg={"runtime_probe_enabled": True},
|
||||
)
|
||||
self.assertEqual(out["runtime_probe"], "gateway")
|
||||
self.assertEqual(
|
||||
out["runtime_facts"]["suggested_backend_url"],
|
||||
"http://observer_web:4000",
|
||||
)
|
||||
self.assertIn("4000", out["subtasks"][0]["edit_goal"])
|
||||
path0 = out["subtasks"][0]["paths"][0]
|
||||
self.assertTrue(path0.endswith("dynamic_conf.yml"))
|
||||
|
||||
def test_suggested_traefik_edit(self) -> None:
|
||||
plan = {
|
||||
"runtime_facts": {
|
||||
"service": "observer_web",
|
||||
"suggested_backend_url": "http://observer_web:4000",
|
||||
"hint": "x",
|
||||
}
|
||||
}
|
||||
fix = suggested_traefik_edit(plan)
|
||||
assert fix is not None
|
||||
self.assertEqual(fix["old_text"], 'url: "http://observer_web:80"')
|
||||
self.assertEqual(fix["new_text"], 'url: "http://observer_web:4000"')
|
||||
|
||||
def test_blocks_blind_without_facts(self) -> None:
|
||||
plan = {"user_goal": "Bad Gateway traefik observer.ift.calentiq.com"}
|
||||
self.assertTrue(devops_blocks_blind_force_edit(plan))
|
||||
plan["runtime_facts"] = {"host": "x", "http_status": 502}
|
||||
self.assertFalse(devops_blocks_blind_force_edit(plan))
|
||||
|
||||
def test_disabled(self) -> None:
|
||||
plan = {"user_goal": "observer.ift.calentiq.com"}
|
||||
out = attach_runtime_probe(
|
||||
plan, user_text="x", cfg={"runtime_probe_enabled": False}
|
||||
)
|
||||
self.assertIsNone(out.get("runtime_facts"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user