Files
alfred/tests/test_prompts_critical.py
T
francwa e45465d52d feat: split resolve_destination, persona-driven prompts, qBittorrent relocation
Destination resolution
- Replace the single ResolveDestinationUseCase with four dedicated
  functions, one per release type:
    resolve_season_destination    (pack season, folder move)
    resolve_episode_destination   (single episode, file move)
    resolve_movie_destination     (movie, file move)
    resolve_series_destination    (multi-season pack, folder move)
- Each returns a dedicated DTO carrying only the fields relevant to
  that release type — no more polymorphic ResolvedDestination with
  half the fields unused depending on the case.
- Looser series folder matching: exact computed-name match is reused
  silently; any deviation (different group, multiple candidates) now
  prompts the user with all options including the computed name.

Agent tools
- Four new tools wrapping the use cases above; old resolve_destination
  removed from the registry.
- New move_to_destination tool: create_folder + move, chained — used
  after a resolve_* call to perform the actual relocation.
- Low-level filesystem_operations module (create_folder, move via mv)
  for instant same-FS renames (ZFS).

Prompt & persona
- New PromptBuilder (alfred/agent/prompt.py) replacing prompts.py:
  identity + personality block, situational expressions, memory
  schema, episodic/STM/config context, tool catalogue.
- Per-user expression system: knowledge/users/common.yaml +
  {username}.yaml are merged at runtime; one phrase per situation
  (greeting/success/error/...) is sampled into the system prompt.

qBittorrent integration
- Credentials now come from settings (qbittorrent_url/username/password)
  instead of hardcoded defaults.
- New client methods: find_by_name, set_location, recheck — the trio
  needed to update a torrent's save path and re-verify after a move.
- Host→container path translation settings (qbittorrent_host_path /
  qbittorrent_container_path) for docker-mounted setups.

Subtitles
- Identifier: strip parenthesized qualifiers (simplified, brazil…) at
  tokenization; new _tokenize_suffix used for the episode_subfolder
  pattern so episode-stem tokens no longer pollute language detection.
- Placer: extract _build_dest_name so it can be reused by the new
  dry_run path in ManageSubtitlesUseCase.
- Knowledge: add yue, ell, ind, msa, rus, vie, heb, tam, tel, tha,
  hin, ukr; add 'fre' to fra; add 'simplified'/'traditional' to zho.

Misc
- LTM workspace: add 'trash' folder slot.
- Default LLM provider switched to deepseek.
- testing/debug_release.py: CLI to parse a release, hit TMDB, and
  dry-run the destination resolution end-to-end.
2026-05-14 05:01:59 +02:00

284 lines
9.5 KiB
Python

"""Critical tests for prompt builder - Tests that would have caught bugs."""
from alfred.agent.prompts import PromptBuilder
from alfred.agent.registry import make_tools
from alfred.settings import settings
class TestPromptBuilderToolsInjection:
"""Critical tests for tools injection in prompts."""
def test_system_prompt_includes_all_tools(self, memory):
"""CRITICAL: Verify all tools are mentioned in system prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
prompt = builder.build_system_prompt()
# Verify each tool is mentioned
for tool_name in tools.keys():
assert tool_name in prompt, (
f"Tool {tool_name} not mentioned in system prompt"
)
def test_tools_spec_contains_all_registered_tools(self, memory):
"""CRITICAL: Verify build_tools_spec() returns all tools."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
specs = builder.build_tools_spec()
spec_names = {spec["function"]["name"] for spec in specs}
tool_names = set(tools.keys())
assert spec_names == tool_names, f"Missing tools: {tool_names - spec_names}"
def test_tools_spec_is_not_empty(self, memory):
"""CRITICAL: Verify tools spec is never empty."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
specs = builder.build_tools_spec()
assert len(specs) > 0, "Tools spec is empty!"
def test_tools_spec_format_matches_openai(self, memory):
"""CRITICAL: Verify tools spec format is OpenAI-compatible."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
specs = builder.build_tools_spec()
for spec in specs:
assert "type" in spec
assert spec["type"] == "function"
assert "function" in spec
assert "name" in spec["function"]
assert "description" in spec["function"]
assert "parameters" in spec["function"]
class TestPromptBuilderMemoryContext:
"""Tests for memory context injection in prompts."""
def test_prompt_includes_current_topic(self, memory):
"""Verify current topic is included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.stm.set_topic("test_topic")
prompt = builder.build_system_prompt()
assert "test_topic" in prompt
def test_prompt_includes_extracted_entities(self, memory):
"""Verify extracted entities are included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.stm.set_entity("test_key", "test_value")
prompt = builder.build_system_prompt()
assert "test_key" in prompt
def test_prompt_includes_search_results(self, memory_with_search_results):
"""Verify search results are included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
prompt = builder.build_system_prompt()
assert "Inception" in prompt
assert "LAST SEARCH" in prompt
def test_prompt_includes_active_downloads(self, memory):
"""Verify active downloads are included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.episodic.add_active_download(
{"task_id": "123", "name": "Test Movie", "progress": 50}
)
prompt = builder.build_system_prompt()
assert "ACTIVE DOWNLOADS" in prompt
assert "Test Movie" in prompt
def test_prompt_includes_recent_errors(self, memory):
"""Verify recent errors are included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.episodic.add_error("test_action", "test error message")
prompt = builder.build_system_prompt()
assert "RECENT ERRORS" in prompt or "error" in prompt.lower()
def test_prompt_includes_configuration(self, memory):
"""Verify configuration is included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.ltm.download_folder = "/test/downloads"
prompt = builder.build_system_prompt()
assert "CONFIGURATION" in prompt or "download_folder" in prompt
def test_prompt_includes_language(self, memory):
"""Verify language is included in prompt."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.stm.set_language("fr")
prompt = builder.build_system_prompt()
assert "fr" in prompt or "LANGUAGE" in prompt
class TestPromptBuilderStructure:
"""Tests for prompt structure and completeness."""
def test_system_prompt_is_not_empty(self, memory):
"""Verify system prompt is never empty."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
prompt = builder.build_system_prompt()
assert len(prompt) > 0
assert prompt.strip() != ""
def test_system_prompt_includes_base_instruction(self, memory):
"""Verify system prompt includes base instruction."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
prompt = builder.build_system_prompt()
assert "assistant" in prompt.lower() or "help" in prompt.lower()
def test_system_prompt_includes_rules(self, memory):
"""Verify system prompt includes important rules."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
prompt = builder.build_system_prompt()
assert "RULES" in prompt or "IMPORTANT" in prompt
def test_system_prompt_includes_examples(self, memory):
"""Verify system prompt includes examples."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
prompt = builder.build_system_prompt()
assert "EXAMPLES" in prompt or "example" in prompt.lower()
def test_tools_description_format(self, memory):
"""Verify tools are properly formatted in description."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
description = builder._format_tools_description()
# Should have tool names and descriptions
for tool_name, _tool in tools.items():
assert tool_name in description
# Should have parameters info
assert "Parameters" in description or "parameters" in description
def test_episodic_context_format(self, memory_with_search_results):
"""Verify episodic context is properly formatted."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
context = builder._format_episodic_context(memory_with_search_results)
assert "LAST SEARCH" in context
assert "Inception" in context
def test_stm_context_format(self, memory):
"""Verify STM context is properly formatted."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.stm.set_topic("test_topic")
memory.stm.set_entity("key", "value")
context = builder._format_stm_context(memory)
assert "TOPIC" in context or "test_topic" in context
assert "ENTITIES" in context or "key" in context
def test_config_context_format(self, memory):
"""Verify config context is properly formatted."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.ltm.download_folder = "/test/downloads"
context = builder._format_config_context(memory)
assert "CONFIGURATION" in context
assert "download_folder" in context
class TestPromptBuilderEdgeCases:
"""Tests for edge cases in prompt building."""
def test_prompt_with_no_memory_context(self, memory):
"""Verify prompt works with empty memory."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
# Memory is empty
prompt = builder.build_system_prompt()
# Should still have base content
assert len(prompt) > 0
assert "assistant" in prompt.lower()
def test_prompt_with_empty_tools(self):
"""Verify prompt handles empty tools dict."""
builder = PromptBuilder({})
prompt = builder.build_system_prompt()
# Should still generate a prompt
assert len(prompt) > 0
def test_tools_spec_with_empty_tools(self):
"""Verify tools spec handles empty tools dict."""
builder = PromptBuilder({})
specs = builder.build_tools_spec()
assert isinstance(specs, list)
assert len(specs) == 0
def test_prompt_with_unicode_in_memory(self, memory):
"""Verify prompt handles unicode in memory."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
memory.stm.set_entity("movie", "Amélie 🎬")
prompt = builder.build_system_prompt()
assert "Amélie" in prompt
assert "🎬" in prompt
def test_prompt_with_long_search_results(self, memory):
"""Verify prompt handles many search results."""
tools = make_tools(settings)
builder = PromptBuilder(tools)
# Add many results
results = [{"name": f"Movie {i}", "seeders": i} for i in range(20)]
memory.episodic.store_search_results("test", results, "torrent")
prompt = builder.build_system_prompt()
# Should include some results but not all (to avoid huge prompts)
assert "Movie 0" in prompt or "Movie 1" in prompt
# Should indicate there are more
assert "more" in prompt.lower() or "..." in prompt