File size: 4,182 Bytes
01bc5cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d0c561
01bc5cd
 
f637227
 
 
 
 
 
 
 
 
01bc5cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""Open Table — a minimal, live-ready 2–3 agent conversation scenario.

Proves the scenario and its cast load from the auto-discovering registry, build,
reset, and that a few conductor ticks produce real ``agent.spoke`` events carrying
the say-vs-think ``text``/``thought``/``mood`` the Fishbowl UI renders — offline, with
no API key (deterministic stub, ADR-0021).  Zero mocks, per repo convention.
"""

from __future__ import annotations

from src.agents.base import ManifestAgent
from src.core.conductor import Conductor
from src.core.ledger_factory import make_ledger
from src.core.registry import default_registry


def _build_conductor(steps: int = 6) -> Conductor:
    reg = default_registry()
    c = Conductor(
        reg.build_scenario("open-table"),
        governor=reg.governor_for("open-table"),
        ledger=make_ledger(),
    )
    c.reset(c.scenario.default_seed)
    c.step(n_ticks=steps)
    return c


class TestOpenTableRegistry:
    def test_scenario_and_cast_are_discovered(self):
        reg = default_registry()
        assert "open-table" in reg.scenarios
        assert {"chat-curious", "chat-skeptic", "chat-host"} <= set(reg.agents)

    def test_scenario_builds_with_its_manifest_cast(self):
        reg = default_registry()
        sc = reg.build_scenario("open-table")
        # The three conversational voices, the table-judge that names the most persuasive
        # of them at the end (the arena verdict, ADR-0029), plus the color commentator.
        assert [a.name for a in sc.agents] == [
            "chat-curious",
            "chat-skeptic",
            "chat-host",
            "table-judge",
            "rafters-critic",
        ]
        assert all(isinstance(a, ManifestAgent) for a in sc.agents)
        assert sc.goal

    def test_profiles_and_ticks_read_from_config(self):
        reg = default_registry()
        sc = reg.build_scenario("open-table")
        by_name = {a.name: a.manifest for a in sc.agents}
        assert by_name["chat-curious"].model_profile == "fast"
        assert by_name["chat-skeptic"].model_profile == "balanced"
        assert by_name["chat-host"].model_profile == "fast"
        assert by_name["chat-curious"].schedule.tick_every == 1
        assert by_name["chat-skeptic"].schedule.tick_every == 1
        assert by_name["chat-host"].schedule.tick_every == 3

    def test_governor_uses_modest_live_safe_caps(self):
        reg = default_registry()
        gov = reg.governor_for("open-table")
        assert gov.max_turns == 40
        assert gov.max_total_calls == 400


class TestOpenTableConversation:
    def test_reset_writes_genesis_with_seed(self):
        c = _build_conductor(steps=0)
        text = " ".join(str(e.payload) for e in c.ledger.events)
        assert c.scenario.default_seed in text

    def test_ticks_produce_spoke_events_with_text(self):
        c = _build_conductor()
        spoke = [e for e in c.ledger.events if e.kind == "agent.spoke"]
        assert spoke, "the talkers should speak within a few ticks"
        assert all(e.payload.get("text") for e in spoke)

    def test_talkers_carry_thought_and_mood(self):
        c = _build_conductor()
        for actor in ("chat-curious", "chat-skeptic"):
            said = [e for e in c.ledger.events if e.kind == "agent.spoke" and e.actor == actor]
            assert said, f"{actor} (tick_every=1) should speak within a few ticks"
            payload = said[-1].payload
            assert payload.get("thought"), "the say-vs-think thought must be in the ledger offline"
            assert payload.get("mood"), "the mood must be in the ledger offline"
            assert payload.get("_raw_fallback") is None, "structured output should be clean offline"

    def test_host_carries_mood_but_no_thought(self):
        # The host opted into [mood] only, so it should not leak a thought field.
        c = _build_conductor(steps=6)
        host = [e for e in c.ledger.events if e.kind == "agent.spoke" and e.actor == "chat-host"]
        assert host, "chat-host (tick_every=3) should speak within a few ticks"
        payload = host[-1].payload
        assert payload.get("mood")
        assert "thought" not in payload