forked from retoor/devplacepy
Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant, autoreplant claim or Refactor. Autoreplant now also respects crop locks through the shared _crop_lock predicate. Plan and measurements in happyplanting.md. Also includes the pending community sync, node network and related work in the tree; the full suite passed (4999) with all of it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,3 +92,135 @@ def test_decode_text():
|
||||
decode_text(b"a\x00b")
|
||||
with pytest.raises(SkillFormatError):
|
||||
decode_text(b"\xff\xfe\xfa")
|
||||
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from devplacepy.net_guard import is_blocked_address
|
||||
from devplacepy.services.skills.fetcher import fetch_skill
|
||||
from tests.conftest import run_async
|
||||
|
||||
SKILL_TEXT = (
|
||||
"---\nname: wire-skill\ndescription: Fetched over a real socket.\n---\n"
|
||||
"# Wire skill\n\nRead [the guide](references/guide.md) and `references/missing.md`.\n"
|
||||
)
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
routes: dict = {}
|
||||
|
||||
def do_GET(self):
|
||||
entry = self.routes.get(self.path.split("?", 1)[0])
|
||||
if entry is None:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
status, headers, body = entry
|
||||
self.send_response(status)
|
||||
for name, value in headers.items():
|
||||
self.send_header(name, value)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *args):
|
||||
return None
|
||||
|
||||
|
||||
def global_address() -> str | None:
|
||||
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
probe.connect(("8.8.8.8", 80))
|
||||
address = probe.getsockname()[0]
|
||||
except OSError:
|
||||
return None
|
||||
finally:
|
||||
probe.close()
|
||||
return None if is_blocked_address(ipaddress.ip_address(address)) else address
|
||||
|
||||
|
||||
needs_global_address = pytest.mark.skipif(
|
||||
global_address() is None,
|
||||
reason="this host has no globally routable IPv4 address, so a local server cannot pass the SSRF guard without patching it",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def origin():
|
||||
port_holder = {}
|
||||
|
||||
def text(body, kind="text/markdown"):
|
||||
return 200, {"Content-Type": kind}, body.encode()
|
||||
|
||||
Handler.routes = {
|
||||
"/ok/": text("<html><body>skill directory</body></html>", "text/html"),
|
||||
"/ok/SKILL.md": text(SKILL_TEXT),
|
||||
"/ok/references/guide.md": text("# Guide\n\nStep one.\n"),
|
||||
"/big/SKILL.md": text("---\nname: big\ndescription: too big\n---\n" + "x" * (limits.SKILL_MD_MAX_BYTES + 10)),
|
||||
"/html/SKILL.md": text("<html><body>not a skill</body></html>", "text/html"),
|
||||
"/bounce/SKILL.md": (302, {"Location": "PLACEHOLDER"}, b""),
|
||||
"/loop/SKILL.md": (302, {"Location": "/loop/SKILL.md"}, b""),
|
||||
}
|
||||
server = ThreadingHTTPServer(("0.0.0.0", 0), Handler)
|
||||
port_holder["port"] = server.server_address[1]
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
yield port_holder["port"]
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_a_skill_is_fetched_over_a_real_socket_with_its_bundled_file(origin):
|
||||
address = global_address()
|
||||
fetched = run_async(fetch_skill(f"http://{address}:{origin}/ok/SKILL.md"))
|
||||
assert fetched.parsed.name == "wire-skill" and fetched.source_url.endswith("/ok/SKILL.md")
|
||||
assert fetched.files == {"references/guide.md": "# Guide\n\nStep one.\n"}
|
||||
assert [(item.path, item.reason) for item in fetched.skipped] == [("references/missing.md", "the server returned 404")]
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_the_directory_address_resolves_to_the_skill_file(origin):
|
||||
address = global_address()
|
||||
fetched = run_async(fetch_skill(f"http://{address}:{origin}/ok/"))
|
||||
assert fetched.parsed.name == "wire-skill"
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_loopback_is_refused_even_though_the_server_is_right_there(origin):
|
||||
with pytest.raises(SkillFetchError, match="private or local"):
|
||||
run_async(fetch_skill(f"http://127.0.0.1:{origin}/ok/SKILL.md"))
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_a_redirect_to_loopback_is_refused_on_the_second_hop(origin):
|
||||
address = global_address()
|
||||
Handler.routes["/bounce/SKILL.md"] = (302, {"Location": f"http://127.0.0.1:{origin}/ok/SKILL.md"}, b"")
|
||||
with pytest.raises(SkillFetchError, match="private or local"):
|
||||
run_async(fetch_skill(f"http://{address}:{origin}/bounce/SKILL.md"))
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_a_redirect_loop_ends_with_a_clean_error(origin):
|
||||
address = global_address()
|
||||
with pytest.raises(SkillFetchError):
|
||||
run_async(fetch_skill(f"http://{address}:{origin}/loop/SKILL.md"))
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_an_oversize_skill_is_cut_at_the_limit_and_html_is_rejected(origin):
|
||||
address = global_address()
|
||||
with pytest.raises(SkillFetchError, match="byte limit"):
|
||||
run_async(fetch_skill(f"http://{address}:{origin}/big/SKILL.md"))
|
||||
with pytest.raises(SkillFetchError, match="HTML page"):
|
||||
run_async(fetch_skill(f"http://{address}:{origin}/html/SKILL.md"))
|
||||
|
||||
|
||||
@needs_global_address
|
||||
def test_a_missing_skill_reports_the_status(origin):
|
||||
address = global_address()
|
||||
with pytest.raises(SkillFetchError, match="404"):
|
||||
run_async(fetch_skill(f"http://{address}:{origin}/nothing/SKILL.md"))
|
||||
|
||||
Reference in New Issue
Block a user