"""Tests for the driver's orchestration logic (external tools are mocked).""" import json from woodshop import driver from woodshop.cli import normalize_anchor def test_anchor_aliases(): assert normalize_anchor("end") == "end_b" assert normalize_anchor("the end") == "end_b" # falls through to default end_b assert normalize_anchor("start") == "end_a" assert normalize_anchor("NEAR") == "end_a" assert normalize_anchor("") == "end_b" def test_dispatch_resolves_dollar_symbols(monkeypatch): """$1/$2 in a multi-op turn resolve to the ids of boards placed this turn.""" seen = [] def fake_run(cmd, stdin=""): if cmd[0] != "pa-execute-tool": return "" name, args = cmd[2], json.loads(cmd[4]) seen.append((name, args)) if name == "wood-place": n = sum(1 for c in seen if c[0] == "wood-place") return json.dumps({"success": True, "output": f"Placed p{n}: a board.", "error": ""}) return json.dumps({"success": True, "output": f"did {name}", "error": ""}) monkeypatch.setattr(driver, "_run", fake_run) calls = [ {"tool": "wood-place", "args": {"stock": "2x4", "length": "2 ft"}}, {"tool": "wood-place", "args": {"stock": "2x4", "length": "2 ft"}}, {"tool": "wood-join", "args": {"part_b": "$2", "to": "$1", "angle": "90"}}, ] driver.dispatch(calls, verbose=False) join_args = next(a for n, a in seen if n == "wood-join") assert join_args["part_b"] == "p2" assert join_args["to"] == "p1" def test_say_pseudo_tool_does_not_dispatch(monkeypatch): calls_made = [] monkeypatch.setattr(driver, "_run", lambda cmd, stdin="": calls_made.append(cmd) or "") msgs = driver.dispatch([{"tool": "say", "args": {"text": "which end?"}}], verbose=False) assert msgs == ["which end?"] assert calls_made == [] # nothing executed def test_interpret_tolerates_fenced_json(monkeypatch): monkeypatch.setattr( driver, "_run", lambda cmd, stdin="": '```json\n[{"tool": "wood-undo", "args": {}}]\n```' if cmd[:2] != ["pa-load-tools", "--tools"] else "[]", ) calls = driver.interpret("undo that", schemas="[]") assert calls == [{"tool": "wood-undo", "args": {}}] def test_summarize_rolls_up_many_ops(): calls = ([{"tool": "wood-place", "args": {}}] * 8 + [{"tool": "wood-join", "args": {}}] * 2 + [{"tool": "wood-stand", "args": {}}] * 4) summary = driver.summarize(calls, [""] * len(calls)) assert "placed 8" in summary assert "joined 2" in summary assert "stood up 4" in summary assert len(summary) < 80 # short enough to speak def test_summarize_speaks_queries_verbatim(): calls = [{"tool": "wood-cutlist", "args": {}}] messages = ["CUT LIST\n 2 x 2x4 ..."] assert driver.summarize(calls, messages).startswith("CUT LIST") def test_summarize_speaks_clarification(): calls = [{"tool": "say", "args": {"text": "which end?"}}] assert driver.summarize(calls, ["which end?"]) == "which end?" def test_interpret_handles_garbage(monkeypatch): monkeypatch.setattr(driver, "_run", lambda cmd, stdin="": "I'm not sure what you mean") calls = driver.interpret("blah", schemas="[]") assert calls[0]["tool"] == "say" def test_extract_calls_ignores_trailing_brackets(): """A greedy [.*] would swallow the trailing '[note]' and fail to parse.""" raw = '[{"tool": "wood-undo", "args": {}}]\n\nLet me know [if that helps].' assert driver._extract_calls(raw) == [{"tool": "wood-undo", "args": {}}] def test_extract_calls_strips_fences_and_handles_object(): assert driver._extract_calls('```json\n{"tool": "wood-clear", "args": {}}\n```') == \ [{"tool": "wood-clear", "args": {}}] def test_extract_calls_returns_none_on_garbage(): assert driver._extract_calls("no json here") is None