odysseus/tests/test_manage_tasks_model_writes.py
Christian-Sidak 17beb95d10 fix(tasks): normalise action=add and nested task object in manage_tasks
Models that are not shown the full schema sometimes emit action="add"
(instead of "create") or wrap task fields in a nested "task" object
rather than providing them at the top level.  Both patterns fell through
to the "Unknown action" error branch, so no task was created even though
the model believed it had succeeded.

This is the same failure class as the manage_skills schema gap fixed in
#4013: the model sees prose describing the tool but not the schema, so
it guesses at the action verb and arg shape.

Changes:
- do_manage_tasks: normalise action="add" to "create" before dispatch
- do_manage_tasks: flatten a nested "task" dict into top-level fields,
  mirroring the existing string-valued "task" alias
- do_manage_tasks: use "name" as the "prompt" fallback when prompt is
  absent, so {"action":"create","name":"foo"} does not return an error
- agent_loop.py: expand the local-model tool snippet for manage_tasks
  with a concrete JSON example and explicit arg descriptions so models
  without schema access emit the right shape on the first attempt

Fixes #5757

Signed-off-by: Christian Sidak <christian@sentineltech.eu>
Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com>
2026-07-27 20:35:45 -07:00

120 lines
3.7 KiB
Python

"""Regression tests for manage_tasks model-write normalisation (#5757).
Models that were not shown the full tool schema (e.g. when RAG selects a
different tool set or a local model is used without function-call schemas)
sometimes emit action="add" instead of action="create", or wrap task fields in
a nested "task" object rather than providing them at the top level. Before the
fix, both patterns fell through to the "unknown action" error branch and the
task was silently not created.
This test module verifies that both calling patterns produce a successfully
created task, matching the same-class fix applied to manage_skills (#4013).
"""
import json
import tempfile
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool
from tests.helpers.import_state import clear_fake_database_modules
clear_fake_database_modules()
import core.database as cdb
from core.database import ScheduledTask
from src.tools.system import do_manage_tasks
_TMPDB = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
_ENGINE = create_engine(
f"sqlite:///{_TMPDB.name}",
connect_args={"check_same_thread": False},
poolclass=NullPool,
)
cdb.Base.metadata.create_all(_ENGINE)
_TS = sessionmaker(bind=_ENGINE, autoflush=False, autocommit=False)
cdb.SessionLocal = _TS
def _count_tasks_named(name):
db = _TS()
try:
return db.query(ScheduledTask).filter(ScheduledTask.name == name).count()
finally:
db.close()
@pytest.mark.asyncio
async def test_action_add_is_treated_as_create():
"""action='add' must be normalised to 'create' so the task is persisted."""
out = await do_manage_tasks(
json.dumps({
"action": "add",
"name": "test task add",
"prompt": "summarise the news",
"task_type": "llm",
"trigger_type": "schedule",
"schedule": "daily",
}),
owner="alice",
)
assert out["exit_code"] == 0, out
assert "Created task" in out["response"]
assert _count_tasks_named("test task add") == 1
@pytest.mark.asyncio
async def test_nested_task_object_is_flattened():
"""{'action':'add','task':{'name':'foo'}} must be treated as create with name=foo."""
out = await do_manage_tasks(
json.dumps({
"action": "add",
"task": {
"name": "test task 2",
},
}),
owner="alice",
)
assert out["exit_code"] == 0, out
assert "Created task" in out["response"]
assert _count_tasks_named("test task 2") == 1
@pytest.mark.asyncio
async def test_nested_task_object_with_prompt():
"""Nested task object with explicit prompt field is handled correctly."""
out = await do_manage_tasks(
json.dumps({
"action": "create",
"task": {
"name": "daily summary",
"prompt": "summarise my emails",
},
"trigger_type": "schedule",
"schedule": "daily",
}),
owner="alice",
)
assert out["exit_code"] == 0, out
assert "Created task" in out["response"]
assert _count_tasks_named("daily summary") == 1
@pytest.mark.asyncio
async def test_name_used_as_prompt_fallback_when_prompt_absent():
"""When prompt is absent but name is present, name is used as the prompt."""
out = await do_manage_tasks(
json.dumps({
"action": "create",
"name": "my simple task",
"task_type": "llm",
"trigger_type": "schedule",
"schedule": "daily",
}),
owner="alice",
)
assert out["exit_code"] == 0, out
assert "Created task" in out["response"]
assert _count_tasks_named("my simple task") == 1