"""Prompt fragments: their variables, their gates, and what gets stored.""" from __future__ import annotations import pytest from lembas.services import prompts, settings_store @pytest.fixture def extra_source(): """Register a fragment source for one test, then take it away again. The registry is module state, so a test that adds to it and does not clean up leaks into every test after it. Restored to **what was there**, not to `[_builtin_source]`. Resetting to the builtin alone also threw away `services/tools.py:_row_source`, registered at import -- so after the first test using this fixture, no custom tool or MCP server contributed a fragment for the rest of the process, and `test_a_custom_tools_guidance_appears_only_when_it_is_offered` passed or failed on file ordering alone. A teardown that quietly removes production wiring is worse than no teardown, because the suite still goes green. """ before = list(prompts._SOURCES) def install(*fragments: prompts.Fragment): prompts.register_source(lambda db: fragments) yield install prompts._SOURCES[:] = before # --- Substitution ------------------------------------------------------------ def test_a_known_variable_is_replaced(): assert prompts.substitute("Hello {{user_name}}.", {"user_name": "Frodo"}) == "Hello Frodo." def test_whitespace_inside_the_braces_is_allowed(): assert prompts.substitute("{{ user_name }}", {"user_name": "Frodo"}) == "Frodo" def test_an_unknown_name_passes_through_exactly_as_typed(): """The fallback the whole syntax choice rests on: a collision with real prompt text degrades to "you get what you wrote".""" text = 'Reply as {"total": 1}, not {{Foo}} or {{a-b}} or ${PATH} or {{unknown}}.' assert prompts.substitute(text, {"user_name": "Frodo"}) == text def test_a_known_but_empty_variable_becomes_nothing(): """Not a pass-through. Pass-through is for names that are not variables, not for variables that happen to be empty -- otherwise an account with no name would send the literal braces to the model.""" assert prompts.substitute("Talking to {{user_name}}.", {"user_name": ""}) == "Talking to ." def test_adjacent_variables_both_expand(): assert prompts.substitute("{{a}}{{b}}", {"a": "1", "b": "2"}) == "12" def test_a_substituted_value_is_never_rescanned(): """A security property, not an accident: {{memories}} carries text a model wrote, and a memory reading "{{skills}}" must not pull in the skill index.""" assert prompts.substitute("{{memories}}", {"memories": "{{skills}}", "skills": "SECRET"}) == ( "{{skills}}" ) def test_a_line_that_was_only_a_variable_disappears(): """So an empty value leaves no hole and no stranded heading.""" assert prompts.substitute("before\n{{memories}}\nafter", {"memories": ""}) == "before\nafter" def test_a_line_with_no_variable_is_left_alone_even_when_blank(): assert prompts.substitute("a\n\nb", {}) == "a\n\nb" # --- Gates ------------------------------------------------------------------- def _assembled(db, **kwargs): return prompts.assemble(db, groups=(prompts.GROUP_CORE, prompts.GROUP_TOOLS), **kwargs) def test_requires_skips_the_whole_fragment(db, extra_source): extra_source( prompts.Fragment( key="core.zz_test", label="t", group=prompts.GROUP_CORE, order=900, default="Bases: {{knowledge_bases}}", requires=("knowledge_bases",), ) ) assert "Bases:" not in _assembled(db, variables={"knowledge_bases": ""}) assert "Bases: contracts" in _assembled(db, variables={"knowledge_bases": "contracts"}) def test_families_gate_a_fragment(db, extra_source): extra_source( prompts.Fragment( key="tool.zz_test", label="t", group=prompts.GROUP_TOOLS, order=900, default="- gated", families=("web_search",), ) ) assert "- gated" not in _assembled(db, variables={}, families=("notes",)) assert "- gated" in _assembled(db, variables={}, families=("web_search",)) def test_when_tools_gates_a_fragment(db): text = _assembled(db, variables={}, has_tools=False) assert "You have tools" not in text assert "You have tools" in _assembled(db, variables={}, has_tools=True) def test_a_run_of_bullets_stays_a_single_list(db): """Five guidance fragments are five lines, not eleven.""" text = prompts.assemble( db, groups=(prompts.GROUP_TOOLS,), variables={"memory_limit": "400"}, families=("web_search", "notes"), ) assert "\n\n- " not in text assert text.count("\n- ") == 1 def test_assembly_is_capped(db): assert len(_assembled(db, variables={}, has_tools=True, limit=100)) <= 102 # --- Storage ----------------------------------------------------------------- def test_an_override_wins_over_the_default(db): prompts.save(db, {"core.heading": "## Rules"}) assert prompts.resolve(db, "core.heading") == "## Rules" assert prompts.is_overridden(db, "core.heading") def test_an_empty_override_turns_a_fragment_off(db): prompts.save(db, {"core.style": ""}) assert "Answer in the language" not in _assembled(db, variables={}) def test_text_equal_to_the_default_is_not_stored(db): """So that improving a default in a later release still reaches an instance whose administrator opened the page and pressed Save.""" default = prompts.catalogue(db)["core.heading"].default prompts.save(db, {"core.heading": default}) assert prompts.stored(db) == {} assert not prompts.is_overridden(db, "core.heading") def test_the_line_endings_a_browser_submits_do_not_count_as_an_edit(db): """A textarea posts CRLF. Without normalising, every fragment would read as edited the moment the page was saved once.""" default = prompts.catalogue(db)["context.memories"].default prompts.save(db, {"context.memories": default.replace("\n", "\r\n")}) assert prompts.stored(db) == {} def test_restoring_the_default_text_removes_the_override(db): default = prompts.catalogue(db)["core.heading"].default prompts.save(db, {"core.heading": "## One"}) prompts.save(db, {"core.heading": default}) assert prompts.stored(db) == {} def test_a_fragment_that_was_not_submitted_keeps_its_override(db): """A fragment can be missing from the page because whatever contributes it is switched off -- a disabled custom tool. A save must not throw its wording away just for not having been on screen.""" prompts.save(db, {"core.heading": "## One", "core.style": "Two"}) prompts.save(db, {"core.heading": "## One"}) assert set(prompts.stored(db)) == {"core.heading", "core.style"} def test_plain_settings_in_the_group_survive_a_save(db): settings_store.update(db, {"max_harness_chars": 500}, key=settings_store.PROMPTS) prompts.save(db, {"core.heading": "## One"}) assert settings_store.get(db, "max_harness_chars", key=settings_store.PROMPTS) == 500 def test_clear_returns_every_fragment_to_its_default(db): prompts.save(db, {"core.heading": "## One"}) prompts.clear(db) assert prompts.stored(db) == {} assert prompts.resolve(db, "core.heading") == "## How to work" def test_overrides_can_be_supplied_without_touching_the_database(db): """What the admin page previews unsaved text with.""" text = _assembled(db, variables={}, overrides={"core.heading": "## Draft"}) assert "## Draft" in text assert prompts.stored(db) == {} # --- The custom-tool seam ---------------------------------------------------- def test_a_registered_source_needs_no_change_anywhere_else(db, extra_source): """The one requirement the whole design exists for: when custom tools land, a tool contributes its guidance by registering a source and nothing else.""" extra_source( prompts.Fragment( key="tool.zz_weather", label="Weather", group=prompts.GROUP_TOOLS, order=500, default="- Check the forecast before answering about weather.", families=("zz_weather",), ) ) assert "tool.zz_weather" in prompts.catalogue(db) assert "Check the forecast" in _assembled(db, variables={}, families=("zz_weather",)) assert any( fragment.key == "tool.zz_weather" for _, _, fragments in prompts.grouped(db) for fragment in fragments ) # And it is editable through the same one write path as a built-in. prompts.save(db, {"tool.zz_weather": "- Ask the sky."}) assert prompts.resolve(db, "tool.zz_weather") == "- Ask the sky." def test_the_first_source_to_claim_a_key_keeps_it(db, extra_source): extra_source( prompts.Fragment( key="core.heading", label="x", group=prompts.GROUP_CORE, default="## Hijacked" ) ) assert prompts.catalogue(db)["core.heading"].default == "## How to work" # --- The catalogue itself ---------------------------------------------------- def test_every_builtin_key_is_unique_and_well_formed(): keys = [fragment.key for fragment in prompts.BUILTIN] assert len(keys) == len(set(keys)) for key in keys: assert prompts.KEY_PATTERN.match(key), key def test_every_variable_a_fragment_names_is_documented(): """The legend is the only place a variable is explained, so a fragment referring to one that is not listed is a fragment nobody can use.""" for fragment in prompts.BUILTIN: for name in (*fragment.variables, *fragment.requires): assert name in prompts.VARIABLE_NAMES, f"{fragment.key} names {name}" for name in prompts.variables_in(fragment.default): assert name in prompts.VARIABLE_NAMES, f"{fragment.key} uses {name}" def test_every_variable_a_fragment_uses_is_declared(): """Otherwise the field's own legend chips would not mention it.""" for fragment in prompts.BUILTIN: for name in prompts.variables_in(fragment.default): assert name in fragment.variables, f"{fragment.key} uses undeclared {name}"