Files
openswarm/backend/tests/test_chat_owns_its_app.py
T

139 lines
6.2 KiB
Python

"""A chat can edit the app it built, without the user re-selecting the card.
ENG-416, production 1.7.9. The agent built an app in a chat and then, in that same chat, said:
"I don't have access to its files in this chat. Click that app's card on your dashboard to select
it, then resend." He selected it; the next turn asked him to "save or reopen it from the dashboard".
Two round trips of clerical work to reintroduce an agent to its own output.
`build_selected_app_context` opens with `if not selected_app_output_ids: return None`, and that list
is populated ONLY by the dashboard element picker. Nothing linked "this session created that output"
to "this session may edit it".
"""
import pytest
from backend.apps.agents.manager.prompt import prompt_context as p_ctx
COMPOSER = "backend/apps/agents/manager/prompt/compose_turn_system_prompt.py"
class P_Out:
def __init__(self, oid, session_id, workspace_id="ws", updated_at="2026-01-01"):
self.id = oid
self.session_id = session_id
self.workspace_id = workspace_id
self.updated_at = updated_at
def p_store(monkeypatch, outs):
import backend.apps.outputs.workspace_io as io
monkeypatch.setattr(io, "load_all", lambda: outs, raising=True)
def test_a_chat_finds_the_app_it_created(monkeypatch):
p_store(monkeypatch, [P_Out("app-1", "chat-A")])
assert p_ctx.apps_created_by_session("chat-A") == ["app-1"]
def test_another_chats_app_is_never_picked_up(monkeypatch):
"""The control. Owning what you built must not become owning everything."""
p_store(monkeypatch, [P_Out("app-1", "chat-A")])
assert p_ctx.apps_created_by_session("chat-B") == []
def test_an_app_with_no_workspace_is_skipped(monkeypatch):
"""`build_selected_app_context` would drop it anyway; offering it would only produce the second
bad message he saw ("that app doesn't have a saved workspace yet")."""
p_store(monkeypatch, [P_Out("app-1", "chat-A", workspace_id=None)])
assert p_ctx.apps_created_by_session("chat-A") == []
def test_newest_first_and_capped(monkeypatch):
"""A chat that built a dozen apps must not dump a dozen workspace blocks into every turn."""
outs = [P_Out(f"app-{i}", "chat-A", updated_at=f"2026-01-{i:02d}") for i in range(1, 9)]
got = p_ctx.apps_created_by_session("chat-A")
p_store(monkeypatch, outs)
got = p_ctx.apps_created_by_session("chat-A")
assert got == ["app-8", "app-7", "app-6"], got
assert len(got) == p_ctx.APPS_OWNED_CAP
def test_a_broken_store_is_silent_not_fatal(monkeypatch):
"""Prompt composition must never die because the outputs dir is unreadable."""
import backend.apps.outputs.workspace_io as io
def p_boom():
raise OSError("disk gone")
monkeypatch.setattr(io, "load_all", p_boom, raising=True)
assert p_ctx.apps_created_by_session("chat-A") == []
def test_no_session_id_asks_the_store_nothing(monkeypatch):
called = {"n": 0}
import backend.apps.outputs.workspace_io as io
monkeypatch.setattr(io, "load_all", lambda: called.__setitem__("n", called["n"] + 1) or [], raising=True)
assert p_ctx.apps_created_by_session(None) == []
assert called["n"] == 0, "a session with no id must not scan every output on disk"
def test_an_explicit_selection_always_wins():
"""A pick is the user speaking; falling back over it would be worse than the bug."""
src = open(COMPOSER).read()
i = src.index("p_app_ids = list(selected_app_output_ids or [])")
body = src[i:i + 400]
assert "if not p_app_ids:" in body, "the fallback must be conditional on nothing being selected"
assert body.index("if not p_app_ids:") > 0
def test_ownership_is_the_CHAT_not_the_sub_agent():
"""`output.session_id` is written from parent_session_id, so a per-dispatch child id matches
nothing and the feature would be silently dead -- the ENG-403 mistake."""
src = open(COMPOSER).read()
i = src.index("apps_created_by_session(")
assert "session.parent_session_id or session.id" in src[i:i + 120]
def test_the_common_path_pays_nothing(monkeypatch):
"""With a selection present the outputs dir must not be scanned at all."""
called = {"n": 0}
import backend.apps.outputs.workspace_io as io
monkeypatch.setattr(io, "load_all", lambda: called.__setitem__("n", called["n"] + 1) or [], raising=True)
src = open(COMPOSER).read()
assert "if not p_app_ids:" in src
# The guard is structural; assert it directly rather than booting a whole turn.
assert called["n"] == 0
# ------------------------------------------------- the scan cost of the fallback (efficiency)
def test_an_unselected_turn_reads_the_outputs_dir_ONCE():
"""Both unselected paths (this chat's apps, and the name-only list) called load_all() for
themselves, so one prompt block cost two full directory reads of every output file. The composer
scans once and hands the rows down."""
src = open(COMPOSER).read()
i = src.index("p_app_ids = list(selected_app_output_ids or [])")
block = src[i:src.index("if app_ctx:", i)]
# Count CALLS, not prose: the comment explaining the fix names load_all() too.
code = "\n".join(ln.split("#")[0] for ln in block.splitlines())
assert code.count("load_all()") == 1, f"exactly one scan per turn, found {code.count('load_all()')}"
assert "apps_created_by_session(session.parent_session_id or session.id, p_outputs)" in block
assert "build_unselected_app_context(p_outputs)" in block
def test_a_selected_turn_scans_NOTHING():
"""The common path: the user picked a card, so neither fallback runs and no directory is read."""
src = open(COMPOSER).read()
i = src.index("p_outputs = None")
guard = src[i:src.index("load_all()", i)]
assert "if not p_app_ids:" in guard, "the scan must sit behind the nothing-selected branch"
def test_both_helpers_still_work_standalone():
"""They are public and called elsewhere; the shared-scan parameter must stay optional."""
import inspect
from backend.apps.agents.manager.prompt.prompt_context import build_unselected_app_context
for fn in (p_ctx.apps_created_by_session, build_unselected_app_context):
p_last = list(inspect.signature(fn).parameters.values())[-1]
assert p_last.default is None, f"{fn.__name__}'s outputs arg must default to None"