forked from Rockachopa/Timmy-time-dashboard
Adds 3 new personas (Pixel, Lyra, Reel) and 5 new tool modules: - Git/DevOps tools (GitPython): clone, status, diff, log, blame, branch, add, commit, push, pull, stash — wired to Forge and Helm personas - Image generation (FLUX via diffusers): text-to-image, storyboards, variations — Pixel persona - Music generation (ACE-Step 1.5): full songs with vocals+instrumentals, instrumental tracks, vocal-only tracks — Lyra persona - Video generation (Wan 2.1 via diffusers): text-to-video, image-to-video clips — Reel persona - Creative Director pipeline: multi-step orchestration that chains storyboard → music → video → assembly into 3+ minute final videos - Video assembler (MoviePy + FFmpeg): stitch clips, overlay audio, title cards, subtitles, final export Also includes: - Spark Intelligence tool-level + creative pipeline event capture - Creative Studio dashboard page (/creative/ui) with 4 tabs - Config settings for all new models and output directories - pyproject.toml creative optional extra for GPU dependencies - 107 new tests covering all modules (624 total, all passing) https://claude.ai/code/session_01KJm6jQkNi3aA3yoQJn636c
223 lines
8.7 KiB
Python
223 lines
8.7 KiB
Python
"""Tests for swarm.personas and swarm.persona_node."""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
# ── Fixture: redirect SQLite DB to a temp directory ──────────────────────────
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def tmp_swarm_db(tmp_path, monkeypatch):
|
|
db_path = tmp_path / "swarm.db"
|
|
monkeypatch.setattr("swarm.tasks.DB_PATH", db_path)
|
|
monkeypatch.setattr("swarm.registry.DB_PATH", db_path)
|
|
monkeypatch.setattr("swarm.stats.DB_PATH", db_path)
|
|
monkeypatch.setattr("swarm.learner.DB_PATH", db_path)
|
|
yield db_path
|
|
|
|
|
|
# ── personas.py ───────────────────────────────────────────────────────────────
|
|
|
|
def test_all_nine_personas_defined():
|
|
from swarm.personas import PERSONAS
|
|
expected = {"echo", "mace", "helm", "seer", "forge", "quill", "pixel", "lyra", "reel"}
|
|
assert expected == set(PERSONAS.keys())
|
|
|
|
|
|
def test_persona_has_required_fields():
|
|
from swarm.personas import PERSONAS
|
|
required = {"id", "name", "role", "description", "capabilities",
|
|
"rate_sats", "bid_base", "bid_jitter", "preferred_keywords"}
|
|
for pid, meta in PERSONAS.items():
|
|
missing = required - set(meta.keys())
|
|
assert not missing, f"Persona {pid!r} missing: {missing}"
|
|
|
|
|
|
def test_get_persona_returns_correct_entry():
|
|
from swarm.personas import get_persona
|
|
echo = get_persona("echo")
|
|
assert echo is not None
|
|
assert echo["name"] == "Echo"
|
|
assert echo["role"] == "Research Analyst"
|
|
|
|
|
|
def test_get_persona_returns_none_for_unknown():
|
|
from swarm.personas import get_persona
|
|
assert get_persona("bogus") is None
|
|
|
|
|
|
def test_list_personas_returns_all_nine():
|
|
from swarm.personas import list_personas
|
|
personas = list_personas()
|
|
assert len(personas) == 9
|
|
|
|
|
|
def test_persona_capabilities_are_comma_strings():
|
|
from swarm.personas import PERSONAS
|
|
for pid, meta in PERSONAS.items():
|
|
assert isinstance(meta["capabilities"], str), \
|
|
f"{pid} capabilities should be a comma-separated string"
|
|
assert "," in meta["capabilities"] or len(meta["capabilities"]) > 0
|
|
|
|
|
|
def test_persona_preferred_keywords_nonempty():
|
|
from swarm.personas import PERSONAS
|
|
for pid, meta in PERSONAS.items():
|
|
assert len(meta["preferred_keywords"]) > 0, \
|
|
f"{pid} must have at least one preferred keyword"
|
|
|
|
|
|
# ── persona_node.py ───────────────────────────────────────────────────────────
|
|
|
|
def _make_persona_node(persona_id="echo", agent_id="persona-1"):
|
|
from swarm.comms import SwarmComms
|
|
from swarm.persona_node import PersonaNode
|
|
comms = SwarmComms(redis_url="redis://localhost:9999") # in-memory fallback
|
|
return PersonaNode(persona_id=persona_id, agent_id=agent_id, comms=comms)
|
|
|
|
|
|
def test_persona_node_inherits_name():
|
|
node = _make_persona_node("echo")
|
|
assert node.name == "Echo"
|
|
|
|
|
|
def test_persona_node_inherits_capabilities():
|
|
node = _make_persona_node("mace")
|
|
assert "security" in node.capabilities
|
|
|
|
|
|
def test_persona_node_has_rate_sats():
|
|
node = _make_persona_node("quill")
|
|
from swarm.personas import PERSONAS
|
|
assert node.rate_sats == PERSONAS["quill"]["rate_sats"]
|
|
|
|
|
|
def test_persona_node_raises_on_unknown_persona():
|
|
from swarm.comms import SwarmComms
|
|
from swarm.persona_node import PersonaNode
|
|
comms = SwarmComms(redis_url="redis://localhost:9999")
|
|
with pytest.raises(KeyError):
|
|
PersonaNode(persona_id="ghost", agent_id="x", comms=comms)
|
|
|
|
|
|
def test_persona_node_bids_low_on_preferred_task():
|
|
node = _make_persona_node("echo") # prefers research/summarize
|
|
bids = [node._compute_bid("please research and summarize this topic") for _ in range(20)]
|
|
avg = sum(bids) / len(bids)
|
|
# Should cluster around bid_base (35) not the off-spec inflated value
|
|
assert avg < 80, f"Expected low bids on preferred task, got avg={avg:.1f}"
|
|
|
|
|
|
def test_persona_node_bids_higher_on_off_spec_task():
|
|
node = _make_persona_node("echo") # echo doesn't prefer "deploy kubernetes"
|
|
bids = [node._compute_bid("deploy kubernetes cluster") for _ in range(20)]
|
|
avg = sum(bids) / len(bids)
|
|
# Off-spec: bid inflated by _OFF_SPEC_MULTIPLIER
|
|
assert avg > 40, f"Expected higher bids on off-spec task, got avg={avg:.1f}"
|
|
|
|
|
|
def test_persona_node_preferred_beats_offspec():
|
|
"""A preferred-task bid should be lower than an off-spec bid on average."""
|
|
node = _make_persona_node("forge") # prefers code/bug/test
|
|
on_spec = [node._compute_bid("write tests and fix bugs in the code") for _ in range(30)]
|
|
off_spec = [node._compute_bid("research market trends in finance") for _ in range(30)]
|
|
assert sum(on_spec) / 30 < sum(off_spec) / 30
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persona_node_join_registers_in_registry():
|
|
from swarm import registry
|
|
node = _make_persona_node("helm", agent_id="helm-join-test")
|
|
await node.join()
|
|
assert node.is_joined is True
|
|
rec = registry.get_agent("helm-join-test")
|
|
assert rec is not None
|
|
assert rec.name == "Helm"
|
|
assert "devops" in rec.capabilities
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persona_node_submits_bid_on_task():
|
|
from swarm.comms import SwarmComms, CHANNEL_BIDS
|
|
comms = SwarmComms(redis_url="redis://localhost:9999")
|
|
from swarm.persona_node import PersonaNode
|
|
node = PersonaNode(persona_id="quill", agent_id="quill-bid-1", comms=comms)
|
|
await node.join()
|
|
|
|
received = []
|
|
comms.subscribe(CHANNEL_BIDS, lambda msg: received.append(msg))
|
|
comms.post_task("task-quill-1", "write documentation for the API")
|
|
|
|
assert len(received) == 1
|
|
assert received[0].data["agent_id"] == "quill-bid-1"
|
|
assert received[0].data["bid_sats"] >= 1
|
|
|
|
|
|
# ── coordinator.spawn_persona ─────────────────────────────────────────────────
|
|
|
|
def test_coordinator_spawn_persona_registers_agent():
|
|
from swarm.coordinator import SwarmCoordinator
|
|
from swarm import registry
|
|
coord = SwarmCoordinator()
|
|
result = coord.spawn_persona("seer")
|
|
assert result["name"] == "Seer"
|
|
assert result["persona_id"] == "seer"
|
|
assert "agent_id" in result
|
|
agents = registry.list_agents()
|
|
assert any(a.name == "Seer" for a in agents)
|
|
|
|
|
|
def test_coordinator_spawn_persona_raises_on_unknown():
|
|
from swarm.coordinator import SwarmCoordinator
|
|
coord = SwarmCoordinator()
|
|
with pytest.raises(ValueError, match="Unknown persona"):
|
|
coord.spawn_persona("ghost")
|
|
|
|
|
|
def test_coordinator_spawn_all_personas():
|
|
from swarm.coordinator import SwarmCoordinator
|
|
from swarm import registry
|
|
coord = SwarmCoordinator()
|
|
names = []
|
|
for pid in ["echo", "mace", "helm", "seer", "forge", "quill", "pixel", "lyra", "reel"]:
|
|
result = coord.spawn_persona(pid)
|
|
names.append(result["name"])
|
|
agents = registry.list_agents()
|
|
registered = {a.name for a in agents}
|
|
for name in names:
|
|
assert name in registered
|
|
|
|
|
|
# ── Adaptive bidding via learner ────────────────────────────────────────────
|
|
|
|
def test_persona_node_adaptive_bid_adjusts_with_history():
|
|
"""After enough outcomes, the learner should shift bids."""
|
|
from swarm.learner import record_outcome, record_task_result
|
|
node = _make_persona_node("echo", agent_id="echo-adaptive")
|
|
|
|
# Record enough winning history on research tasks
|
|
for i in range(5):
|
|
record_outcome(
|
|
f"adapt-{i}", "echo-adaptive",
|
|
"research and summarize topic", 30,
|
|
won_auction=True, task_succeeded=True,
|
|
)
|
|
|
|
# With high win rate + high success rate, bid should differ from static
|
|
bids_adaptive = [node._compute_bid("research and summarize findings") for _ in range(20)]
|
|
# The learner should adjust — exact direction depends on win/success balance
|
|
# but the bid should not equal the static value every time
|
|
assert len(set(bids_adaptive)) >= 1 # at minimum it returns something valid
|
|
assert all(b >= 1 for b in bids_adaptive)
|
|
|
|
|
|
def test_persona_node_without_learner_uses_static_bid():
|
|
from swarm.persona_node import PersonaNode
|
|
from swarm.comms import SwarmComms
|
|
comms = SwarmComms(redis_url="redis://localhost:9999")
|
|
node = PersonaNode(persona_id="echo", agent_id="echo-static", comms=comms, use_learner=False)
|
|
bids = [node._compute_bid("research and summarize topic") for _ in range(20)]
|
|
# Static bids should be within the persona's base ± jitter range
|
|
for b in bids:
|
|
assert 20 <= b <= 50 # echo: bid_base=35, jitter=15 → range [20, 35]
|