Owner's correction to 1.4.0: a model's character is per (model, person), and only the description and the notes stay instance-wide. Two people talking to one model are not talking to the same personality, and neither can see the other's. The administrator's box becomes the DEFAULT, resolved by `personas.effective` as a fallback and never as a layer -- two personalities at once contradict each other with nothing to say which is losing, which is the reasoning behind "system prompts replace, never stack". `persona_write` takes no argument naming a model or a person; both come from the ToolContext, so it can only write the character it has with whoever it is talking to, and it never touches the default. Impressions move to their own table. Not a `kind` column: 1.4.0 shipped `UNIQUE(model_key, owner_id)`, SQLite cannot alter a constraint and this schema is additive-only, so a discriminator would leave an upgraded instance unable to hold both rows for one pair. That leaves the first MANUAL_STEPS entry this project has had -- the two shapes are indistinguishable, so nothing rewrites them: a repair would be guessing at text that is read back in the first person. TWO BUGS FROM A PHONE `min-width` beats both `width` and `max-width` -- CSS clamps width to max-width and then raises the result to min-width -- so `.canvas` and `.terminal` were 384px wide on every screen narrower than that, their `min(…, 100vw)` cap overruled, and `.inspector` had no cap at all on a width that is a preference draggable to 2400px. None of it scrolled sideways, because all three are `position: fixed` and fixed overflow does not extend the scrollable area -- which is exactly why the 1.1.0 narrow pass reported these pages clean. `min-width: 0` in the overlay query, full width below the phone breakpoint, tablet column kept. And the install button now says why it is absent. Measured against the live instance: the manifest meets every Chrome criterion and the blocker is a certificate from a private CA, so the origin is not trustworthy, the service worker is refused and no install is offered. `base.html` had been swallowing that with an empty catch -- which kept the page working, the reason it was there, and threw away the only evidence. It now records the outcome and `app.js` turns it into a sentence naming the certificate, which is the cause the old hint did not mention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
587 lines
23 KiB
Python
587 lines
23 KiB
Python
"""A model's personality with one person, and what it makes of them.
|
|
|
|
Both are per (model, person), in two tables — so the assertions that matter most
|
|
are about the boundaries between them: the administrator's default must not leak
|
|
*into* somebody who has their own, one account's personality and impression must
|
|
be invisible and undeletable to another, and a personality must not be reachable
|
|
through the impression route or the other way round.
|
|
|
|
A model-written text about a person that the person cannot read is the thing this
|
|
must not become, so the settings page is tested as part of the feature rather than
|
|
as decoration.
|
|
|
|
The safety story for self-modification is a record and a way back rather than a
|
|
gate, which is `SkillRevision`'s argument; the revision tests are where that is
|
|
pinned. An impression deliberately has no history — see its own docstring.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from lembas.db.models import (
|
|
AUTHOR_MODEL,
|
|
AUTHOR_USER,
|
|
ROLE_USER,
|
|
Chat,
|
|
Connection,
|
|
Impression,
|
|
Model,
|
|
Persona,
|
|
User,
|
|
)
|
|
from lembas.services import harness as harness_service
|
|
from lembas.services import personas as personas_service
|
|
from lembas.services import settings_store
|
|
from lembas.services import tools as tools_service
|
|
from lembas.services.crypto import encrypt
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def personality_allowed(db, registered):
|
|
settings_store.update(db, {"default_permissions": {"tools.persona": True}})
|
|
connection = Connection(
|
|
name="Test", base_url="http://127.0.0.1:1", api_key_encrypted=encrypt("")
|
|
)
|
|
db.add(connection)
|
|
db.commit()
|
|
for index, name in enumerate(("test-model", "other-model")):
|
|
db.add(
|
|
Model(
|
|
connection_id=connection.id,
|
|
model_id=name,
|
|
display_name=name,
|
|
position=index,
|
|
capabilities_json={"tools": True},
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
|
|
def _user(db) -> User:
|
|
return db.scalars(select(User).order_by(User.created_at)).first()
|
|
|
|
|
|
def _second_user(db) -> User:
|
|
"""A row directly, the way `test_sharing.py` makes its three accounts."""
|
|
from lembas.security.passwords import hash_password
|
|
|
|
user = User(
|
|
name="Sam", email="s@example.test", password_hash=hash_password("x"), role="user"
|
|
)
|
|
db.add(user)
|
|
db.commit()
|
|
return user
|
|
|
|
|
|
def _chat(db, model_id: str = "test-model", user: User | None = None) -> Chat:
|
|
chat = Chat(user_id=(user or _user(db)).id, title="t", model_id=model_id)
|
|
db.add(chat)
|
|
db.commit()
|
|
return chat
|
|
|
|
|
|
async def _run(db, chat: Chat, name: str, args: dict):
|
|
user = db.get(User, chat.user_id)
|
|
resolved = tools_service.resolve_tools(db, chat, user)
|
|
context = tools_service.context_for(db, user, chat, tools=resolved)
|
|
return await tools_service.run_tool(context, name, json.dumps(args))
|
|
|
|
|
|
# --- The two halves are not the same row --------------------------------------
|
|
def test_a_personality_and_an_impression_are_separate_rows(db):
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=user, content="I am terse.")
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=user, content="They test things."
|
|
)
|
|
|
|
assert personas_service.block(db, "test-model", user) == "I am terse."
|
|
assert personas_service.view_block(db, "test-model", user) == "They test things."
|
|
|
|
|
|
def test_a_personality_is_each_persons_own(db):
|
|
"""The change asked for in 1.5.0: a character is something a model works out
|
|
with somebody, so two people do not share one."""
|
|
first = _user(db)
|
|
second = _second_user(db)
|
|
personas_service.write(db, model_key="test-model", owner=first, content="Blunt with them.")
|
|
personas_service.write(db, model_key="test-model", owner=second, content="Careful here.")
|
|
|
|
assert personas_service.block(db, "test-model", first) == "Blunt with them."
|
|
assert personas_service.block(db, "test-model", second) == "Careful here."
|
|
|
|
|
|
def test_a_person_without_one_of_their_own_gets_the_default(db):
|
|
"""What makes the administrator's default mean anything."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=None, content="The default.")
|
|
assert personas_service.block(db, "test-model", user) == "The default."
|
|
|
|
|
|
def test_the_default_stops_applying_once_somebody_has_their_own(db):
|
|
"""A starting point and not a layer. Two personalities at once would
|
|
contradict each other and nobody could tell which was losing -- the same
|
|
reasoning that makes system prompts replace rather than stack."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=None, content="The default.")
|
|
personas_service.write(db, model_key="test-model", owner=user, content="Mine.")
|
|
|
|
assert personas_service.block(db, "test-model", user) == "Mine."
|
|
# And the default is untouched, for everybody who has not got their own.
|
|
assert personas_service.block(db, "test-model", _second_user(db)) == "The default."
|
|
|
|
|
|
def test_a_missing_personality_does_not_fall_back_to_an_impression(db):
|
|
"""They answer different questions. A fallback between them would put "what
|
|
it makes of you" where "who it is" belongs, in the first person."""
|
|
user = _user(db)
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=user, content="They test things."
|
|
)
|
|
assert personas_service.block(db, "test-model", user) == ""
|
|
|
|
|
|
def test_each_model_keeps_its_own_read_of_the_same_person(db):
|
|
user = _user(db)
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=user, content="Impatient."
|
|
)
|
|
personas_service.write_impression(
|
|
db, model_key="other-model", owner=user, content="Thorough."
|
|
)
|
|
|
|
assert personas_service.view_block(db, "test-model", user) == "Impatient."
|
|
assert personas_service.view_block(db, "other-model", user) == "Thorough."
|
|
|
|
|
|
def test_one_accounts_impression_is_invisible_to_another(db):
|
|
first = _user(db)
|
|
second = _second_user(db)
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=first, content="Writes tests."
|
|
)
|
|
|
|
assert personas_service.view_block(db, "test-model", second) == ""
|
|
assert [row.content for row in personas_service.impressions_for(db, second)] == []
|
|
assert [row.content for row in personas_service.impressions_for(db, first)] == [
|
|
"Writes tests."
|
|
]
|
|
|
|
|
|
def test_one_accounts_personality_is_invisible_to_another(db):
|
|
first = _user(db)
|
|
second = _second_user(db)
|
|
personas_service.write(db, model_key="test-model", owner=first, content="Mine alone.")
|
|
|
|
assert [row.content for row in personas_service.personas_of(db, second)] == []
|
|
assert [row.content for row in personas_service.personas_of(db, first)] == ["Mine alone."]
|
|
|
|
|
|
# --- Writing, keeping, and going back -----------------------------------------
|
|
def test_every_change_keeps_what_was_there(db):
|
|
personas_service.write(db, model_key="test-model", owner=None, content="First.")
|
|
personas_service.write(
|
|
db, model_key="test-model", owner=None, content="Second.", note="thought again"
|
|
)
|
|
|
|
row = personas_service.get(db, "test-model", None)
|
|
assert row.content == "Second."
|
|
assert [r.content for r in row.revisions] == ["First."]
|
|
assert row.revisions[0].note == "thought again"
|
|
|
|
|
|
def test_writing_the_same_text_again_keeps_no_revision(db):
|
|
"""Otherwise a model that rewrites itself identically every turn fills the
|
|
history and pushes the real "before" out of it."""
|
|
personas_service.write(db, model_key="test-model", owner=None, content="Same.")
|
|
personas_service.write(db, model_key="test-model", owner=None, content="Same.")
|
|
assert personas_service.get(db, "test-model", None).revisions == []
|
|
|
|
|
|
def test_reverting_keeps_the_text_it_replaced(db):
|
|
"""An undo that cannot be undone is a second way to lose the same work."""
|
|
personas_service.write(db, model_key="test-model", owner=None, content="First.")
|
|
personas_service.write(db, model_key="test-model", owner=None, content="Second.")
|
|
row = personas_service.get(db, "test-model", None)
|
|
|
|
personas_service.revert(db, row, row.revisions[0])
|
|
|
|
# The session is built with `expire_on_commit=False`, so a committed change
|
|
# is not visible through an object already loaded here until it is expired.
|
|
db.expire_all()
|
|
row = personas_service.get(db, "test-model", None)
|
|
assert row.content == "First."
|
|
assert "Second." in [r.content for r in row.revisions]
|
|
assert row.author == AUTHOR_USER
|
|
|
|
|
|
def test_the_history_is_bounded(db):
|
|
for index in range(personas_service.MAX_REVISIONS + 8):
|
|
personas_service.write(db, model_key="test-model", owner=None, content=f"v{index}")
|
|
db.expire_all()
|
|
row = personas_service.get(db, "test-model", None)
|
|
assert len(row.revisions) <= personas_service.MAX_REVISIONS
|
|
|
|
|
|
def test_an_over_long_text_is_trimmed_rather_than_refused(db):
|
|
"""`memories.py`'s rule: a write the model could not have known was too long
|
|
should not cost it the turn."""
|
|
row = personas_service.write(
|
|
db, model_key="test-model", owner=None, content="x" * 5000
|
|
)
|
|
assert len(row.content) == personas_service.MAX_PERSONA_CHARS
|
|
|
|
|
|
def test_an_impression_is_held_to_the_shorter_limit(db):
|
|
"""Shorter on purpose: it is a standing impression, not a file."""
|
|
row = personas_service.write_impression(
|
|
db, model_key="test-model", owner=_user(db), content="y" * 5000
|
|
)
|
|
assert len(row.content) == personas_service.MAX_VIEW_CHARS
|
|
|
|
|
|
def test_the_row_survives_the_model_row_being_replaced(db):
|
|
"""Keyed on the model's own id and not on the `Model` primary key, because
|
|
"Test & refresh" deletes a model the endpoint has stopped listing and gives
|
|
it a new primary key when it returns. A personality must not be collateral."""
|
|
personas_service.write(db, model_key="test-model", owner=None, content="I am terse.")
|
|
row = db.scalar(select(Model).where(Model.model_id == "test-model"))
|
|
connection_id = row.connection_id
|
|
db.delete(row)
|
|
db.commit()
|
|
db.add(Model(connection_id=connection_id, model_id="test-model"))
|
|
db.commit()
|
|
|
|
assert personas_service.block(db, "test-model", None) == "I am terse."
|
|
|
|
|
|
# --- What the tools write -----------------------------------------------------
|
|
async def test_persona_write_can_only_rewrite_the_answering_model(db):
|
|
"""There is deliberately no argument naming a model or a person: both are
|
|
taken from the context, so a call cannot reach another model's character or
|
|
somebody else's copy of this one's."""
|
|
user = _user(db)
|
|
chat = _chat(db, "test-model")
|
|
outcome = await _run(db, chat, "persona_write", {"content": "I am blunt.", "why": "learnt"})
|
|
|
|
assert outcome.event["status"] == "ok"
|
|
assert personas_service.block(db, "test-model", user) == "I am blunt."
|
|
assert personas_service.get(db, "other-model", user) is None
|
|
|
|
|
|
async def test_persona_write_never_touches_the_default(db):
|
|
"""A model editing everybody's starting point from inside one conversation is
|
|
a much larger thing than editing its own character."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=None, content="The default.")
|
|
chat = _chat(db, "test-model")
|
|
|
|
await _run(db, chat, "persona_write", {"content": "Mine now."})
|
|
|
|
assert personas_service.block(db, "test-model", user) == "Mine now."
|
|
assert personas_service.get(db, "test-model", None).content == "The default."
|
|
|
|
|
|
async def test_persona_write_is_recorded_as_the_models_own_work(db):
|
|
chat = _chat(db)
|
|
await _run(db, chat, "persona_write", {"content": "Mine."})
|
|
assert personas_service.get(db, "test-model", _user(db)).author == AUTHOR_MODEL
|
|
|
|
|
|
async def test_an_empty_persona_write_is_refused_rather_than_erasing(db):
|
|
"""It replaces rather than appends, so an empty call would be a wipe — and a
|
|
model that has been talked into one turn of nonsense should not be able to
|
|
end its own character in it."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=user, content="I am terse.")
|
|
chat = _chat(db)
|
|
|
|
outcome = await _run(db, chat, "persona_write", {"content": " "})
|
|
|
|
assert outcome.event["status"] == "error"
|
|
assert personas_service.block(db, "test-model", user) == "I am terse."
|
|
|
|
|
|
async def test_impression_write_is_keyed_on_the_person_as_well_as_the_model(db):
|
|
chat = _chat(db)
|
|
await _run(db, chat, "impression_write", {"content": "They want the short answer."})
|
|
|
|
user = _user(db)
|
|
assert personas_service.view_block(db, "test-model", user) == "They want the short answer."
|
|
# Not the personality, which is a row in the other table.
|
|
assert personas_service.get(db, "test-model", user) is None
|
|
|
|
|
|
async def test_an_empty_impression_write_clears_it(db):
|
|
"""The opposite of the persona, on purpose: "I have no standing view of this
|
|
person" is a legitimate state, and "I have no character" is not."""
|
|
chat = _chat(db)
|
|
await _run(db, chat, "impression_write", {"content": "Something."})
|
|
await _run(db, chat, "impression_write", {"content": ""})
|
|
assert personas_service.view_block(db, "test-model", _user(db)) == ""
|
|
|
|
|
|
async def test_the_tool_is_offered_only_with_the_capability_and_the_permission(db):
|
|
chat = _chat(db)
|
|
user = _user(db)
|
|
assert "persona_write" in tools_service.resolve_tools(db, chat, user).by_name
|
|
|
|
model = db.scalar(select(Model).where(Model.model_id == "test-model"))
|
|
model.capabilities_json = {"tools": True, "tool_persona": False}
|
|
db.commit()
|
|
assert "persona_write" not in tools_service.resolve_tools(db, chat, user).by_name
|
|
|
|
model.capabilities_json = {"tools": True}
|
|
user.role = ROLE_USER
|
|
settings_store.update(db, {"default_permissions": {"tools.persona": False}})
|
|
db.commit()
|
|
assert "persona_write" not in tools_service.resolve_tools(db, chat, user).by_name
|
|
|
|
|
|
# --- What reaches the prompt --------------------------------------------------
|
|
def _values(db, chat: Chat, *, families: list[str]) -> dict[str, str]:
|
|
offered = [
|
|
tool.schema
|
|
for tool in tools_service.registry(db).values()
|
|
if tools_service.gate_of(tool.family) in families
|
|
]
|
|
return harness_service.context_variables(db, db.get(User, chat.user_id), offered, chat)
|
|
|
|
|
|
def test_both_variables_are_gated_on_the_family(db):
|
|
"""A model that may not keep either has no business being handed them, and
|
|
the query should not happen at all on an instance that does not use this."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=user, content="I am terse.")
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=user, content="Impatient."
|
|
)
|
|
chat = _chat(db)
|
|
|
|
without = _values(db, chat, families=["memory"])
|
|
assert without["persona"] == ""
|
|
assert without["person_view"] == ""
|
|
|
|
with_it = _values(db, chat, families=["persona"])
|
|
assert with_it["persona"] == "I am terse."
|
|
assert with_it["person_view"] == "Impatient."
|
|
|
|
|
|
def test_a_switched_off_persona_reads_as_absent(db):
|
|
user = _user(db)
|
|
row = personas_service.write(db, model_key="test-model", owner=user, content="I am terse.")
|
|
row.enabled = False
|
|
db.commit()
|
|
assert personas_service.block(db, "test-model", user) == ""
|
|
|
|
|
|
def test_the_fragments_vanish_when_there_is_nothing_to_say(db):
|
|
chat = _chat(db)
|
|
preamble = harness_service.compose(
|
|
db,
|
|
_user(db),
|
|
[
|
|
tool.schema
|
|
for tool in tools_service.registry(db).values()
|
|
if tools_service.gate_of(tool.family) == "persona"
|
|
],
|
|
chat,
|
|
)
|
|
assert "Who you are" not in preamble
|
|
assert "What you have made of them" not in preamble
|
|
|
|
|
|
def test_the_fragments_carry_the_texts_when_there_are_some(db):
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=user, content="I argue back.")
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=user, content="Likes brevity."
|
|
)
|
|
chat = _chat(db)
|
|
|
|
preamble = harness_service.compose(
|
|
db,
|
|
user,
|
|
[
|
|
tool.schema
|
|
for tool in tools_service.registry(db).values()
|
|
if tools_service.gate_of(tool.family) == "persona"
|
|
],
|
|
chat,
|
|
)
|
|
assert "I argue back." in preamble
|
|
assert "Likes brevity." in preamble
|
|
# The persona comes before the impression: a fact the person stated should be
|
|
# read before an opinion the model formed about them.
|
|
assert preamble.index("I argue back.") < preamble.index("Likes brevity.")
|
|
|
|
|
|
# --- The screens --------------------------------------------------------------
|
|
def test_the_person_can_read_and_delete_both(client, db, registered):
|
|
"""The whole reason writing either is acceptable. Model-written text about
|
|
somebody that they cannot see is not something this should hold."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=user, content="Blunt with them.")
|
|
personas_service.write_impression(
|
|
db, model_key="test-model", owner=user, content="Wants brevity."
|
|
)
|
|
|
|
page = client.get("/settings")
|
|
assert "Who each model is with you" in page.text
|
|
assert "Blunt with them." in page.text
|
|
assert "What models make of you" in page.text
|
|
assert "Wants brevity." in page.text
|
|
|
|
persona = personas_service.personas_of(db, user)[0]
|
|
impression = personas_service.impressions_for(db, user)[0]
|
|
client.post(f"/api/library/personalities/{persona.id}/delete", follow_redirects=False)
|
|
client.post(f"/api/library/impressions/{impression.id}/delete", follow_redirects=False)
|
|
db.expire_all()
|
|
assert personas_service.personas_of(db, user) == []
|
|
assert personas_service.impressions_for(db, user) == []
|
|
|
|
|
|
def test_deleting_a_personality_falls_back_to_the_default(client, db):
|
|
"""Which is what makes offering the delete reasonable: it is a reset, not the
|
|
loss of the model's character."""
|
|
user = _user(db)
|
|
personas_service.write(db, model_key="test-model", owner=None, content="The default.")
|
|
personas_service.write(db, model_key="test-model", owner=user, content="Mine.")
|
|
|
|
row = personas_service.personas_of(db, user)[0]
|
|
client.post(f"/api/library/personalities/{row.id}/delete", follow_redirects=False)
|
|
|
|
db.expire_all()
|
|
assert personas_service.block(db, "test-model", user) == "The default."
|
|
|
|
|
|
def test_nobody_can_delete_somebody_elses(client, db):
|
|
second = _second_user(db)
|
|
persona = personas_service.write(
|
|
db, model_key="test-model", owner=second, content="Theirs."
|
|
)
|
|
impression = personas_service.write_impression(
|
|
db, model_key="test-model", owner=second, content="Theirs too."
|
|
)
|
|
|
|
assert client.post(
|
|
f"/api/library/personalities/{persona.id}/delete", follow_redirects=False
|
|
).status_code == 404
|
|
assert client.post(
|
|
f"/api/library/impressions/{impression.id}/delete", follow_redirects=False
|
|
).status_code == 404
|
|
|
|
db.expire_all()
|
|
assert personas_service.get(db, "test-model", second) is not None
|
|
assert personas_service.impression(db, "test-model", second) is not None
|
|
|
|
|
|
def test_the_default_cannot_be_deleted_from_the_settings_page(client, db):
|
|
"""`owner_id IS NULL` is the instance's, not this person's. An id from that
|
|
half arriving at the reader's route must be refused on ownership rather than
|
|
found by existence."""
|
|
row = personas_service.write(db, model_key="test-model", owner=None, content="Instance.")
|
|
|
|
response = client.post(
|
|
f"/api/library/personalities/{row.id}/delete", follow_redirects=False
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
db.expire_all()
|
|
assert personas_service.get(db, "test-model", None).content == "Instance."
|
|
|
|
|
|
def test_a_personality_cannot_be_deleted_through_the_impression_route(client, db):
|
|
"""Two tables, two routes, and an id from one must not resolve in the other --
|
|
`db.get` on the wrong class returns None, which is the answer that matters."""
|
|
user = _user(db)
|
|
row = personas_service.write(db, model_key="test-model", owner=user, content="Mine.")
|
|
|
|
response = client.post(
|
|
f"/api/library/impressions/{row.id}/delete", follow_redirects=False
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
db.expire_all()
|
|
assert personas_service.block(db, "test-model", user) == "Mine."
|
|
|
|
|
|
def test_an_administrator_can_read_write_and_revert_the_default(client, db):
|
|
model = db.scalar(select(Model).where(Model.model_id == "test-model"))
|
|
|
|
client.post(
|
|
f"/admin/models/{model.id}/persona",
|
|
data={"content": "I am terse."},
|
|
follow_redirects=False,
|
|
)
|
|
client.post(
|
|
f"/admin/models/{model.id}/persona",
|
|
data={"content": "I am not terse at all."},
|
|
follow_redirects=False,
|
|
)
|
|
db.expire_all()
|
|
row = personas_service.get(db, "test-model", None)
|
|
assert row.content == "I am not terse at all."
|
|
assert row.author == AUTHOR_USER
|
|
|
|
page = client.get(f"/admin/models/{model.id}/edit")
|
|
assert "I am not terse at all." in page.text
|
|
assert "Earlier defaults" in page.text
|
|
|
|
client.post(
|
|
f"/admin/models/{model.id}/persona/revert",
|
|
data={"revision_id": row.revisions[0].id},
|
|
follow_redirects=False,
|
|
)
|
|
db.expire_all()
|
|
assert personas_service.get(db, "test-model", None).content == "I am terse."
|
|
|
|
|
|
def test_a_revision_of_another_model_cannot_be_restored_onto_this_one(client, db):
|
|
"""Checked against this persona rather than merely existing, or an id from
|
|
another model's history transplants its personality."""
|
|
personas_service.write(db, model_key="other-model", owner=None, content="Theirs first.")
|
|
personas_service.write(db, model_key="other-model", owner=None, content="Theirs second.")
|
|
personas_service.write(db, model_key="test-model", owner=None, content="Mine.")
|
|
foreign = personas_service.get(db, "other-model", None).revisions[0]
|
|
model = db.scalar(select(Model).where(Model.model_id == "test-model"))
|
|
|
|
response = client.post(
|
|
f"/admin/models/{model.id}/persona/revert",
|
|
data={"revision_id": foreign.id},
|
|
follow_redirects=False,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
db.expire_all()
|
|
assert personas_service.block(db, "test-model", None) == "Mine."
|
|
|
|
|
|
def test_clearing_the_default_from_the_admin_page_removes_it(client, db):
|
|
model = db.scalar(select(Model).where(Model.model_id == "test-model"))
|
|
personas_service.write(db, model_key="test-model", owner=None, content="I am terse.")
|
|
|
|
client.post(f"/admin/models/{model.id}/persona", data={"content": ""}, follow_redirects=False)
|
|
|
|
db.expire_all()
|
|
assert personas_service.get(db, "test-model", None) is None
|
|
assert db.scalars(select(Persona)).all() == []
|
|
|
|
|
|
def test_clearing_the_default_leaves_everybodys_own_alone(client, db):
|
|
"""They diverged from it; removing the starting point is not removing them."""
|
|
user = _user(db)
|
|
model = db.scalar(select(Model).where(Model.model_id == "test-model"))
|
|
personas_service.write(db, model_key="test-model", owner=None, content="The default.")
|
|
personas_service.write(db, model_key="test-model", owner=user, content="Mine.")
|
|
|
|
client.post(f"/admin/models/{model.id}/persona", data={"content": ""}, follow_redirects=False)
|
|
|
|
db.expire_all()
|
|
assert personas_service.block(db, "test-model", user) == "Mine."
|
|
assert db.scalars(select(Impression)).all() == []
|