build-small-hackathon/hackathon-advisor
16
1from tests.helpers import load_test_index2 3from hackathon_advisor.agent import AdvisorEngine4from hackathon_advisor.tool_contracts import ToolCall, ToolResolution5 6 7class StaticPlanner:8 backend = "test"9 model_id = "static"10 11 def __init__(self, call: ToolCall) -> None:12 self.call = call13 14 def plan(self, message: str, state: dict) -> ToolResolution:15 return ToolResolution(status="valid", call=self.call, errors=())16 17 18def test_agent_scores_and_persists_idea() -> None:19 index = load_test_index()20 engine = AdvisorEngine(index)21 22 result = engine.turn("A local-first archive cartographer for family photos", {})23 24 assert result.score is not None25 assert result.state["ideas"]26 assert result.state["ideas"][0]["score"] is not None27 assert "goal_fit" in result.state["ideas"][0]["score"]28 assert "prize_fit" not in result.state["ideas"][0]["score"]29 assert "goal_fit" in result.artifact["seal"]30 assert "prize_fit" not in result.artifact["seal"]31 assert result.state["trace"]32 assert result.state["last_tool_resolution"]["call"]["name"] == "save_idea"33 assert result.state["trace"][0]["tool_resolution"]["call"]["name"] == "save_idea"34 assert result.state["last_artifact"]["title"] == result.artifact["title"]35 assert result.state["ideas"][0]["artifact"]["title"] == result.artifact["title"]36 assert result.artifact["wood_map"]["caption"]37 assert {dot["kind"] for dot in result.artifact["wood_map"]["dots"]} >= {"idea", "echo", "inked"}38 assert result.score.to_dict()["echoes"][0]["page_number"] >= 139 assert "page " in result.response40 assert result.response41 42 43def test_agent_finds_whitespace() -> None:44 index = load_test_index()45 engine = AdvisorEngine(index)46 47 result = engine.turn("write bolder and find whitespace", {})48 49 assert result.whitespace50 assert result.score is not None51 assert result.artifact["verdict"] == "UNWRITTEN"52 assert result.state["ideas"][0]["title"] == result.whitespace[0].label53 54 55def test_gap_command_explores_unused_whitespace() -> None:56 index = load_test_index()57 engine = AdvisorEngine(index)58 59 first = engine.turn("write bolder and find whitespace", {})60 second = engine.turn("write bolder and find whitespace", first.state)61 62 assert len(second.state["ideas"]) == 263 assert first.whitespace[0].label != second.whitespace[0].label64 assert second.state["ideas"][-1]["title"] == second.whitespace[0].label65 assert second.state["current_whitespace"]["label"] == second.whitespace[0].label66 67 68def test_agent_preserves_canonical_jargon_case() -> None:69 index = load_test_index()70 engine = AdvisorEngine(index)71 72 result = engine.turn("use neutron and mini cpm on zero gpu", {})73 74 assert "MiniCPM5" in result.artifact["title"]75 assert "ZeroGPU" in result.artifact["title"]76 77 78def test_plan_command_uses_current_idea() -> None:79 index = load_test_index()80 engine = AdvisorEngine(index)81 82 first = engine.turn("A local-first archive cartographer for family photos", {})83 planned = engine.turn("make a build plan", first.state)84 85 assert planned.plan86 assert planned.artifact["title"] == first.artifact["title"]87 assert planned.state["ideas"][0]["title"] == first.artifact["title"]88 assert all("Record the trace" not in step for step in planned.plan)89 assert all("session trace" not in step for step in planned.plan)90 91 92def test_non_plan_turns_clear_stale_build_plan() -> None:93 index = load_test_index()94 engine = AdvisorEngine(index)95 96 first = engine.turn("A local-first archive cartographer for family photos", {})97 planned = engine.turn("make a build plan", first.state)98 project = engine.turn("read project lolaby", planned.state)99 second = engine.turn("A hands-on science coach for kitchen experiments", planned.state)100 101 assert planned.state["last_plan"]102 assert "last_plan" not in project.state103 assert "last_plan" not in second.state104 105 106def test_plan_and_rank_do_not_create_placeholder_ideas() -> None:107 index = load_test_index()108 engine = AdvisorEngine(index)109 110 planned = engine.turn("make a build plan", {})111 ranked = engine.turn("compare ideas", planned.state)112 113 assert planned.state["ideas"] == []114 assert ranked.state["ideas"] == []115 assert "Write one project instinct first" in planned.response116 assert "No idea pages" in ranked.response117 assert planned.tool_events[0].name == "make_plan"118 assert ranked.tool_events[0].name == "compare_ideas"119 120 121def test_plan_uses_profile_context() -> None:122 index = load_test_index()123 engine = AdvisorEngine(index)124 state = {125 "profile": {126 "skills": "frontend prototyping",127 "time": "one evening",128 "preferences": "quiet dashboards",129 "constraints": "CPU-only Space",130 }131 }132 133 first = engine.turn("A local-first archive cartographer for family photos", state)134 planned = engine.turn("make a build plan", first.state)135 136 assert any("one evening" in step for step in planned.plan)137 assert any("frontend prototyping" in step for step in planned.plan)138 assert any("CPU-only Space" in step for step in planned.plan)139 assert any("quiet dashboards" in step for step in planned.plan)140 141 142def test_distinct_idea_turns_append_to_board() -> None:143 index = load_test_index()144 engine = AdvisorEngine(index)145 146 first = engine.turn("A local-first archive cartographer for family photos", {})147 second = engine.turn("write bolder and find whitespace", first.state)148 149 assert len(second.state["ideas"]) == 2150 assert second.state["ideas"][0]["title"] == first.artifact["title"]151 assert second.state["ideas"][1]["title"] == second.artifact["title"]152 assert second.state["ideas"][0]["artifact"]["title"] == first.artifact["title"]153 assert second.state["ideas"][1]["artifact"]["title"] == second.artifact["title"]154 155 156def test_compare_ideas_reranks_board_and_selects_winner() -> None:157 index = load_test_index()158 engine = AdvisorEngine(index)159 160 first = engine.turn("A local-first archive cartographer for family photos", {})161 second = engine.turn("write bolder and find whitespace", first.state)162 ranked = engine.turn("compare ideas", second.state)163 164 assert ranked.score is not None165 assert ranked.artifact["title"] == ranked.state["ideas"][0]["title"]166 assert ranked.state["current_idea_id"] == ranked.state["ideas"][0]["id"]167 assert ranked.state["ideas"][0]["score"]["overall"] >= ranked.state["ideas"][1]["score"]["overall"]168 assert all(idea["artifact"]["title"] == idea["title"] for idea in ranked.state["ideas"])169 assert ranked.plan170 assert "Ranked pages:" in ranked.response171 assert ranked.tool_events[0].name == "compare_ideas"172 173 174def test_plan_preserves_unwritten_whitespace_verdict() -> None:175 index = load_test_index()176 engine = AdvisorEngine(index)177 178 whitespace = engine.turn("write bolder and find whitespace", {})179 planned = engine.turn("make a build plan", whitespace.state)180 181 assert whitespace.artifact["verdict"] == "UNWRITTEN"182 assert planned.artifact["title"] == whitespace.artifact["title"]183 assert planned.artifact["verdict"] == "UNWRITTEN"184 185 186def test_planner_get_project_drives_project_response() -> None:187 index = load_test_index()188 engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("get_project", {"id": "lolaby"})))189 190 result = engine.turn("read lolaby", {})191 192 assert result.projects193 assert result.projects[0].slug == "lolaby"194 assert result.tool_events[0].name == "get_project"195 196 197def test_rule_project_reference_does_not_create_or_score_idea() -> None:198 index = load_test_index()199 engine = AdvisorEngine(index)200 first = engine.turn("A local-first archive cartographer for family photos", {})201 202 result = engine.turn("read project lolaby", first.state)203 204 assert result.projects205 assert result.projects[0].slug == "lolaby"206 assert result.score is None207 assert result.artifact == {}208 assert len(result.state["ideas"]) == 1209 assert result.state["ideas"][0]["title"] == first.artifact["title"]210 assert result.state["last_artifact"]["title"] == first.artifact["title"]211 assert result.state["last_tool_resolution"]["call"]["name"] == "get_project"212 213 214def test_planner_profile_and_goals_update_state() -> None:215 index = load_test_index()216 planned = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})217 planned = AdvisorEngine(index).turn("make a build plan", planned.state)218 assert planned.state["last_plan"]219 profile_engine = AdvisorEngine(220 index,221 planner=StaticPlanner(ToolCall("update_profile", {"field": "skills", "value": "frontend"})),222 )223 profile = profile_engine.turn("remember this", planned.state)224 target_engine = AdvisorEngine(225 index,226 planner=StaticPlanner(ToolCall("set_goals", {"goals": ["Off the Grid", "Field Notes"]})),227 )228 targeted = target_engine.turn("set goals", profile.state)229 230 assert targeted.state["profile"]["skills"] == "frontend"231 assert targeted.state["goals"] == ["Off the Grid", "Field Notes"]232 assert "last_plan" not in profile.state233 assert "last_plan" not in targeted.state234 assert "Local-first, Build notes" in targeted.response235 236 237def test_goal_update_invalidates_current_idea_artifact() -> None:238 index = load_test_index()239 first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})240 first = AdvisorEngine(index).turn("make a build plan", first.state)241 assert first.state["last_plan"]242 target_engine = AdvisorEngine(243 index,244 planner=StaticPlanner(ToolCall("set_goals", {"goals": ["Field Notes"]})),245 )246 247 targeted = target_engine.turn("set goals", first.state)248 249 idea = targeted.state["ideas"][0]250 assert idea["score"] is None251 assert idea["artifact"] is None252 assert "last_artifact" not in targeted.state253 assert "last_plan" not in targeted.state254 255 256def test_session_goals_apply_to_new_and_current_ideas() -> None:257 index = load_test_index()258 engine = AdvisorEngine(index)259 state = {"goals": ["Field Notes"]}260 261 first = engine.turn("A local-first archive cartographer for family photos", state)262 first_idea = first.state["ideas"][0]263 planned = engine.turn("make a build plan", first.state)264 265 assert first_idea["goals"] == ["Field Notes"]266 assert all("LoRA" not in step for step in planned.plan)267 268 269def test_well_tuned_goal_adds_training_step_to_plan() -> None:270 index = load_test_index()271 engine = AdvisorEngine(index)272 state = {"goals": ["Well-Tuned"]}273 274 first = engine.turn("A local-first archive cartographer for family photos", state)275 planned = engine.turn("make a build plan", first.state)276 277 assert first.state["ideas"][0]["goals"] == ["Well-Tuned"]278 assert any("LoRA" in step for step in planned.plan)279 assert all("advisor turns" not in step for step in planned.plan)280 281 282def test_planner_score_idea_scores_current_idea() -> None:283 index = load_test_index()284 first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})285 engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("score_idea", {})))286 287 scored = engine.turn("score it", first.state)288 289 assert scored.score is not None290 assert scored.artifact["title"] == first.artifact["title"]291 292 293def test_turn_stream_emits_ordered_progress_events() -> None:294 index = load_test_index()295 engine = AdvisorEngine(index)296 297 events = list(engine.turn_stream("A local-first archive cartographer for family photos", {}))298 types = [event["type"] for event in events]299 300 assert types[0] == "start"301 assert types[-1] == "done"302 assert "token" in types303 # the planning stage is announced before any tool runs, and tools stream as they execute304 assert types.index("stage") < types.index("tool_event")305 tool_events = [event for event in events if event["type"] == "tool_event"]306 assert [event["name"] for event in tool_events] == ["save_idea", "search_projects", "score_idea"]307 assert events[-1]["state"]["ideas"]308 309 310def test_turn_stream_done_matches_blocking_turn() -> None:311 # idea ids are randomly generated, so compare the deterministic surface of the turn.312 index = load_test_index()313 streamed = list(AdvisorEngine(index).turn_stream("write bolder and find whitespace", {}))314 done = next(event for event in streamed if event["type"] == "done")315 blocking = AdvisorEngine(index).turn("write bolder and find whitespace", {})316 317 assert done["response"] == blocking.response318 assert done["score"] == (blocking.score.to_dict() if blocking.score else None)319 assert done["plan"] == blocking.plan320 assert [item["label"] for item in done["whitespace"]] == [321 item.label for item in blocking.whitespace322 ]323 assert [idea["title"] for idea in done["state"]["ideas"]] == [324 idea["title"] for idea in blocking.state["ideas"]325 ]326 327 328def test_turn_accepts_injected_resolution() -> None:329 index = load_test_index()330 engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("score_idea", {})))331 injected = ToolResolution(status="valid", call=ToolCall("list_projects", {"sort": "likes"}), errors=())332 333 result = engine.turn("score it", {}, resolution=injected)334 335 # the injected list_projects call wins over the planner's score_idea call336 assert result.state["last_tool_resolution"]["call"]["name"] == "list_projects"337 