# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
import importlib
import os
import uuid
from pathlib import Path
import pytest
def test_get_talent_configs_generators():
"""Test that system generators are discovered with source field."""
talent = importlib.import_module("think.talent")
generators = talent.get_talent_configs(type="generate")
assert "flow" in generators
info = generators["flow"]
assert os.path.basename(info["path"]) == "flow.md"
assert isinstance(info["color"], str)
assert isinstance(info["mtime"], int)
assert "title" in info
assert "occurrences" in info
# New: check source field
assert info.get("source") == "system"
def test_get_output_name():
"""Test generator key to filename conversion."""
talent = importlib.import_module("think.talent")
# System generators: key unchanged
assert talent.get_output_name("activity") == "activity"
assert talent.get_output_name("flow") == "flow"
# App generators: _app_name format
assert talent.get_output_name("chat:sentiment") == "_chat_sentiment"
assert talent.get_output_name("my_app:weekly_summary") == "_my_app_weekly_summary"
def test_get_talent_configs_app_discovery(tmp_path, monkeypatch):
"""Test that app generators are discovered from apps/*/talent/."""
talent = importlib.import_module("think.talent")
# Create a fake app with a generator
app_dir = tmp_path / "apps" / "test_app" / "talent"
app_dir.mkdir(parents=True)
# Create generator files with frontmatter
(app_dir / "custom_generator.md").write_text(
'{\n "title": "Custom Generator",\n "color": "#ff0000"\n}\n\nTest prompt'
)
# Also create workspace.html to make it a valid app (not strictly required for generators)
(tmp_path / "apps" / "test_app" / "workspace.html").write_text("
Test
")
# For now, just verify system generators have correct source
generators = talent.get_talent_configs(type="generate")
for key, info in generators.items():
if ":" not in key:
assert info.get("source") == "system", f"{key} should have source=system"
def test_get_talent_configs_by_schedule():
"""Test filtering generators by schedule."""
talent = importlib.import_module("think.talent")
# Get daily generators
daily = talent.get_talent_configs(type="generate", schedule="daily")
assert len(daily) > 0
for key, meta in daily.items():
assert meta.get("schedule") == "daily", f"{key} should have schedule=daily"
# Get segment generators
segment = talent.get_talent_configs(type="generate", schedule="segment")
assert len(segment) > 0
for key, meta in segment.items():
assert meta.get("schedule") == "segment", f"{key} should have schedule=segment"
# Verify no overlap
assert not set(daily.keys()) & set(segment.keys()), (
"daily and segment should not overlap"
)
# Unknown schedule returns empty dict
assert talent.get_talent_configs(type="generate", schedule="hourly") == {}
assert talent.get_talent_configs(type="generate", schedule="") == {}
def test_get_talent_configs_include_disabled(monkeypatch):
"""Test include_disabled parameter."""
talent = importlib.import_module("think.talent")
# Get generators without disabled (default)
without_disabled = talent.get_talent_configs(type="generate", schedule="daily")
# Get generators with disabled included
with_disabled = talent.get_talent_configs(
type="generate", schedule="daily", include_disabled=True
)
# Should have at least as many with disabled included
assert len(with_disabled) >= len(without_disabled)
def test_scheduled_generators_have_valid_schedule():
"""Test that scheduled generators have valid schedule field.
Generators with a schedule field must have valid values
('segment', 'daily', or 'activity'). Some generators (like importer) have
output but no schedule - they're used for ad-hoc processing, not scheduled runs.
"""
talent = importlib.import_module("think.talent")
generators = talent.get_talent_configs(type="generate")
valid_schedules = ("segment", "daily", "activity", "weekly")
for key, meta in generators.items():
sched = meta.get("schedule")
if sched is not None:
assert sched in valid_schedules, (
f"Generator '{key}' has invalid schedule '{sched}'"
)
def test_sense_in_segment_schedule():
"""Test that sense generator exists in segment schedule at priority 5."""
talent = importlib.import_module("think.talent")
generators = talent.get_talent_configs(type="generate", schedule="segment")
assert "sense" in generators
sense = generators["sense"]
assert sense.get("priority") == 5, "sense should be at priority 5"
sources = sense.get("load", {})
assert sources.get("transcripts") is True, "sense should include transcripts"
assert sources.get("percepts") is True, "sense should include percepts"
def _write_temp_talent_prompt(stem: str, frontmatter: str) -> Path:
talent_dir = Path(__file__).resolve().parent.parent / "talent"
prompt_path = talent_dir / f"{stem}.md"
prompt_path.write_text(
f"{frontmatter}\n\nTemporary test prompt\n", encoding="utf-8"
)
return prompt_path
def test_get_talent_configs_raises_on_missing_type_with_output():
talent = importlib.import_module("think.talent")
stem = f"test_missing_type_output_{uuid.uuid4().hex}"
prompt_path = _write_temp_talent_prompt(
stem,
'{\n "schedule": "daily",\n "priority": 10,\n "output": "md"\n}',
)
try:
with pytest.raises(
ValueError, match=rf"Prompt '{stem}'.*missing required 'type'"
):
talent.get_talent_configs(include_disabled=True)
finally:
prompt_path.unlink(missing_ok=True)
def test_get_talent_configs_allows_missing_type_with_tools():
talent = importlib.import_module("think.talent")
stem = f"test_missing_type_tools_{uuid.uuid4().hex}"
prompt_path = _write_temp_talent_prompt(
stem,
'{\n "schedule": "daily",\n "priority": 10,\n "tools": "journal"\n}',
)
try:
configs = talent.get_talent_configs(include_disabled=True)
assert stem in configs
assert configs[stem].get("type") is None
finally:
prompt_path.unlink(missing_ok=True)
def test_get_talent_configs_raises_when_generate_missing_output():
talent = importlib.import_module("think.talent")
stem = f"test_generate_missing_output_{uuid.uuid4().hex}"
prompt_path = _write_temp_talent_prompt(
stem,
'{\n "type": "generate",\n "schedule": "daily",\n "priority": 10\n}',
)
try:
with pytest.raises(
ValueError,
match=rf"Prompt '{stem}'.*type='generate'.*missing required 'output'",
):
talent.get_talent_configs(include_disabled=True)
finally:
prompt_path.unlink(missing_ok=True)
def test_get_talent_configs_allows_cogitate_without_tools():
talent = importlib.import_module("think.talent")
stem = f"test_cogitate_missing_tools_{uuid.uuid4().hex}"
prompt_path = _write_temp_talent_prompt(
stem,
'{\n "type": "cogitate",\n "schedule": "daily",\n "priority": 10\n}',
)
try:
configs = talent.get_talent_configs(include_disabled=True)
assert stem in configs
assert configs[stem]["type"] == "cogitate"
finally:
prompt_path.unlink(missing_ok=True)
def test_get_talent_configs_type_generate_returns_only_generate():
talent = importlib.import_module("think.talent")
generators = talent.get_talent_configs(type="generate")
assert generators, "Expected at least one generate prompt"
assert all(meta.get("type") == "generate" for meta in generators.values())
def test_get_talent_configs_type_cogitate_returns_only_cogitate():
talent = importlib.import_module("think.talent")
cogitate_prompts = talent.get_talent_configs(type="cogitate")
assert cogitate_prompts, "Expected at least one cogitate prompt"
assert all(meta.get("type") == "cogitate" for meta in cogitate_prompts.values())