Skip to content

Commit 593b958

Browse files
fix(plan): improve plan module test coverage and fix hint bug (#1220)
--------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 597be68 commit 593b958

File tree

3 files changed

+344
-18
lines changed

3 files changed

+344
-18
lines changed

src/agentscope/plan/_plan_notebook.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,19 @@ def __call__(self, plan: Plan | None) -> str | None:
150150
),
151151
)
152152

153+
elif n_done + n_abandoned == len(plan.subtasks):
154+
# All subtasks are done or abandoned
155+
hint = self.at_the_end.format(
156+
plan=plan.to_markdown(),
157+
)
158+
153159
elif n_in_progress == 0 and n_done > 0:
154160
# No subtask is in_progress, and some subtasks are done
155161
hint = self.when_no_subtask_in_progress.format(
156162
plan=plan.to_markdown(),
157163
index=n_done,
158164
)
159165

160-
elif n_done + n_abandoned == len(plan.subtasks):
161-
# All subtasks are done or abandoned
162-
hint = self.at_the_end.format(
163-
plan=plan.to_markdown(),
164-
)
165-
166166
if hint:
167167
return f"{self.hint_prefix}{hint}{self.hint_suffix}"
168168

@@ -334,13 +334,16 @@ async def revise_current_plan(
334334
try:
335335
subtask_idx = int(subtask_idx)
336336
except ValueError:
337-
pass
338-
339-
if not isinstance(subtask_idx, int):
340-
response.append(
341-
f"Invalid type for argument 'subtask_idx'. "
342-
f"Expected 'int', but got '{type(subtask_idx)}'.",
343-
)
337+
return ToolResponse(
338+
content=[
339+
TextBlock(
340+
type="text",
341+
text=f"Error: Invalid type for argument "
342+
f"'subtask_idx'. Expected 'int', but got "
343+
f"'{type(subtask_idx)}'.",
344+
),
345+
],
346+
)
344347

345348
if action not in ["add", "revise", "delete"]:
346349
response.append(
@@ -356,20 +359,22 @@ async def revise_current_plan(
356359

357360
self._validate_current_plan()
358361

359-
# validate subtask_idx
360-
if action != "add" and subtask_idx >= len(self.current_plan.subtasks):
362+
if action != "add" and subtask_idx >= len(
363+
self.current_plan.subtasks,
364+
):
361365
response.append(
362-
f"Invalid subtask_idx '{subtask_idx}' for action '{action}'. "
363-
f"Must be between 0 "
366+
f"Invalid subtask_idx '{subtask_idx}' for action "
367+
f"'{action}'. Must be between 0 "
364368
f"and {len(self.current_plan.subtasks) - 1}.",
365369
)
366370

367371
if action == "add" and not (
368372
0 <= subtask_idx <= len(self.current_plan.subtasks)
369373
):
374+
max_idx = len(self.current_plan.subtasks)
370375
response.append(
371376
f"Invalid subtask_idx '{subtask_idx}' for action 'add'. "
372-
f"Must be between 0 and {len(self.current_plan.subtasks)}.",
377+
f"Must be between 0 and {max_idx}.",
373378
)
374379

375380
if response:

tests/plan_test.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,175 @@ async def test_serialization(self) -> None:
610610
self.assertIsNotNone(
611611
agent.plan_notebook.current_plan,
612612
)
613+
614+
async def test_hint_generator_all_states(self) -> None:
615+
"""Test DefaultPlanToHint covers all plan states."""
616+
from agentscope.plan._plan_notebook import DefaultPlanToHint
617+
618+
hint_gen = DefaultPlanToHint()
619+
620+
# State 1: No plan
621+
hint = hint_gen(None)
622+
assert hint is not None
623+
self.assertIn("create_plan", hint)
624+
625+
# State 2: All todo
626+
plan = Plan(
627+
name="Test",
628+
description="desc",
629+
expected_outcome="outcome",
630+
subtasks=[
631+
SubTask(name="t1", description="d", expected_outcome="e"),
632+
],
633+
)
634+
hint = hint_gen(plan)
635+
assert hint is not None
636+
self.assertIn("subtask_idx=0", hint)
637+
638+
# State 3: In progress
639+
plan.subtasks[0].state = "in_progress"
640+
hint = hint_gen(plan)
641+
assert hint is not None
642+
self.assertIn("finish_subtask", hint)
643+
644+
# State 4: Some done, none in progress
645+
plan.subtasks[0].state = "done"
646+
plan.subtasks.append(
647+
SubTask(name="t2", description="d", expected_outcome="e"),
648+
)
649+
hint = hint_gen(plan)
650+
assert hint is not None
651+
self.assertIn("first 1", hint.lower())
652+
653+
# State 5: All done
654+
plan.subtasks[1].state = "done"
655+
hint = hint_gen(plan)
656+
assert hint is not None
657+
self.assertIn("finish_plan", hint)
658+
659+
# State 6: Mix done and abandoned
660+
plan.subtasks[1].state = "abandoned"
661+
hint = hint_gen(plan)
662+
assert hint is not None
663+
self.assertIn("finish_plan", hint)
664+
665+
async def test_error_paths(self) -> None:
666+
"""Test error handling paths to improve coverage."""
667+
notebook = PlanNotebook()
668+
669+
# Test operations without plan
670+
with self.assertRaises(ValueError):
671+
await notebook.revise_current_plan(0, "delete")
672+
673+
# Create plan for subsequent tests
674+
await notebook.create_plan(
675+
name="Test",
676+
description="d",
677+
expected_outcome="e",
678+
subtasks=[
679+
SubTask(name="t1", description="d", expected_outcome="e"),
680+
SubTask(name="t2", description="d", expected_outcome="e"),
681+
],
682+
)
683+
684+
# Invalid types and values
685+
# type: ignore
686+
res = await notebook.revise_current_plan("invalid", "delete")
687+
self.assertIn("Invalid type", res.content[0].get("text", ""))
688+
689+
# type: ignore
690+
res = await notebook.revise_current_plan(0, "bad_action")
691+
self.assertIn("Invalid action", res.content[0].get("text", ""))
692+
693+
res = await notebook.revise_current_plan(0, "add", None)
694+
self.assertIn("must be provided", res.content[0].get("text", ""))
695+
696+
res = await notebook.revise_current_plan(999, "delete")
697+
self.assertIn("Invalid subtask_idx", res.content[0].get("text", ""))
698+
699+
res = await notebook.update_subtask_state(999, "in_progress")
700+
self.assertIn("Invalid subtask_idx", res.content[0].get("text", ""))
701+
702+
# type: ignore
703+
res = await notebook.update_subtask_state(0, "bad_state")
704+
self.assertIn("Invalid state", res.content[0].get("text", ""))
705+
706+
# State constraints
707+
res = await notebook.update_subtask_state(1, "in_progress")
708+
self.assertIn("not done yet", res.content[0].get("text", ""))
709+
710+
await notebook.update_subtask_state(0, "in_progress")
711+
res = await notebook.update_subtask_state(1, "in_progress")
712+
self.assertIn("not done yet", res.content[0].get("text", ""))
713+
714+
res = await notebook.finish_subtask(1, "outcome")
715+
self.assertIn("not done yet", res.content[0].get("text", ""))
716+
717+
async def test_edge_cases(self) -> None:
718+
"""Test edge cases and special scenarios."""
719+
notebook = PlanNotebook()
720+
721+
# Finish plan when no plan exists
722+
res = await notebook.finish_plan("done", "outcome")
723+
self.assertIn("no plan", res.content[0].get("text", "").lower())
724+
725+
# Create and replace plan
726+
await notebook.create_plan(
727+
"P1",
728+
"d",
729+
"e",
730+
[SubTask(name="t", description="d", expected_outcome="e")],
731+
)
732+
res = await notebook.create_plan(
733+
"P2",
734+
"d",
735+
"e",
736+
[SubTask(name="t", description="d", expected_outcome="e")],
737+
)
738+
self.assertIn("replaced", res.content[0].get("text", "").lower())
739+
740+
# Auto-activate next subtask
741+
assert notebook.current_plan is not None
742+
notebook.current_plan.subtasks.append(
743+
SubTask(name="t2", description="d", expected_outcome="e"),
744+
)
745+
await notebook.update_subtask_state(0, "in_progress")
746+
res = await notebook.finish_subtask(0, "done")
747+
self.assertIn("next subtask", res.content[0].get("text", "").lower())
748+
assert notebook.current_plan is not None
749+
self.assertEqual(
750+
notebook.current_plan.subtasks[1].state,
751+
"in_progress",
752+
)
753+
754+
# Historical plans
755+
await notebook.finish_subtask(1, "done")
756+
await notebook.finish_plan("done", "final")
757+
res = await notebook.view_historical_plans()
758+
self.assertIn("P2", res.content[0].get("text", ""))
759+
760+
plans = await notebook.storage.get_plans()
761+
res = await notebook.recover_historical_plan(plans[0].id)
762+
self.assertIn("recovered", res.content[0].get("text", "").lower())
763+
764+
res = await notebook.recover_historical_plan("bad_id")
765+
self.assertIn("cannot find", res.content[0].get("text", "").lower())
766+
767+
# Hooks
768+
called = []
769+
770+
async def hook(_nb: PlanNotebook, _p: Plan | None) -> None:
771+
called.append(True)
772+
773+
notebook.register_plan_change_hook("test", hook)
774+
await notebook.create_plan(
775+
"P3",
776+
"d",
777+
"e",
778+
[SubTask(name="t", description="d", expected_outcome="e")],
779+
)
780+
self.assertTrue(called)
781+
782+
notebook.remove_plan_change_hook("test")
783+
with self.assertRaises(ValueError):
784+
notebook.remove_plan_change_hook("bad_hook")

0 commit comments

Comments
 (0)