Two selects that never wrote anything, and a queue
The approval card in Auto mode and the missing /effort were one bug. Both
selects hung their hx-patch on an empty sibling form reached by form="…",
and htmx binds a trigger to the annotated element: change fires on the
select and bubbles to its ancestors, which a sibling is not. The live rows
read agent_mode=manual and params_json={} while the browser showed Auto and
Effort: high. policy.py was never involved.
The verb moves onto the control; the empty form stays as value scoping,
which is the half of the CLAUDE.md note that was right. conftest gains
control_named so a test asserts the element carrying the name carries the
verb, rather than asserting the markup that was there throughout.
The composer's highlight was a third instance of the same carelessness in
CSS: .tok-mention is written for the transcript and scoped to nothing, so
the mirror painted its token in accent-coloured monospace over the
textarea's own text. Scoped under .msg; the mirror restates transparency
and font rather than inheriting them, and bleeds by box-shadow.
/effort is now offered before the first prompt and _new_chat reads it.
/index re-walks the project directory on demand, file_write drops the
listing it just invalidated, and the index ladder falls through to SFTP on
a host that refuses exec instead of returning nothing.
A second message during a reply is queued rather than starting a second
concurrent generation: a real Message row with queued set, so it survives a
restart and can be withdrawn. _drain hands one on at the end of a reply,
_inject takes one in at a tool-round boundary so an agent can be steered
mid-task. Stop leaves the queue undelivered. The terminal's Auto toggle
becomes off/copy/send, and send posts straight to the chat without touching
the composer.
@ now offers notes, skills, this chat's attachments and a URL to fetch; a
knowledge base attaches as a reference rather than a copy. copy_document
carries provenance, which was the one attach path that dropped it.
Also fixes an unrelated live bug: the round loop compared against the
global MAX_ROUNDS of 3 while sizing itself from the agent budget of 40, so
agent replies stopped after three rounds and reported forty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,143 @@ def test_a_plain_chat_gets_a_picker_with_no_file_half(client: TestClient, db, re
|
||||
assert "In the project" not in response.text
|
||||
|
||||
|
||||
# --- What `@` offers where there is no machine at all ------------------------
|
||||
def _library(db):
|
||||
"""A note, a skill and a base belonging to the registered reader."""
|
||||
from lembas.db.models import KnowledgeBase, Note, Skill
|
||||
|
||||
owner = db.scalars(select(User)).first()
|
||||
note = Note(owner_id=owner.id, title="Mallorn notes", body="Golden leaves.")
|
||||
skill = Skill(
|
||||
owner_id=owner.id, name="bake-lembas", description="How to bake it", body="Steps."
|
||||
)
|
||||
base = KnowledgeBase(owner_id=owner.id, name="Contracts")
|
||||
db.add_all([note, skill, base])
|
||||
db.commit()
|
||||
return note, skill, base
|
||||
|
||||
|
||||
def test_notes_and_skills_are_offered_in_a_plain_chat(client: TestClient, db, registered):
|
||||
"""A chat with no SSH connection has no project files, which is exactly why
|
||||
the rest of the library has to be reachable there."""
|
||||
_library(db)
|
||||
|
||||
body = client.get("/api/files/mention-picker", params={"q": "mallorn"}).text
|
||||
assert "Mallorn notes" in body
|
||||
|
||||
body = client.get("/api/files/mention-picker", params={"q": "lembas"}).text
|
||||
assert "bake-lembas" in body
|
||||
|
||||
|
||||
def test_a_knowledge_base_is_only_offered_inside_a_chat(
|
||||
client: TestClient, db, registered, make_chat
|
||||
):
|
||||
"""There is nothing to attach it to before a chat exists -- the same reason
|
||||
project files are absent on the new-chat screen."""
|
||||
_library(db)
|
||||
chat_id = make_chat()
|
||||
|
||||
assert "Contracts" not in client.get("/api/files/mention-picker").text
|
||||
assert "Contracts" in client.get(
|
||||
"/api/files/mention-picker", params={"chat_id": chat_id}
|
||||
).text
|
||||
|
||||
|
||||
def test_a_url_is_offered_as_a_page_to_read(client: TestClient, db, registered):
|
||||
body = client.get(
|
||||
"/api/files/mention-picker", params={"q": "https://tolkien.test/mallorn"}
|
||||
).text
|
||||
|
||||
assert "Fetch this page" in body
|
||||
assert "https://tolkien.test/mallorn" in body
|
||||
|
||||
|
||||
def test_a_note_arrives_with_its_text_and_its_name(client: TestClient, db, registered, make_chat):
|
||||
note, _skill, _base = _library(db)
|
||||
chat_id = make_chat()
|
||||
|
||||
response = client.post(
|
||||
"/api/files/from-note", data={"note_id": note.id, "chat_id": chat_id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
attachment = db.scalars(select(Attachment)).one()
|
||||
assert "Golden leaves." in attachment.extracted_text
|
||||
# Provenance, for the reason a project file carries it: a model handed four
|
||||
# documents cannot name one back when asked to work on it.
|
||||
assert attachment.source_label == "Note"
|
||||
assert attachment.source_path == "Mallorn notes"
|
||||
|
||||
|
||||
def test_a_skill_can_be_handed_over_directly(client: TestClient, db, registered, make_chat):
|
||||
"""The index is in the harness and `skill_get` fetches on demand -- but only
|
||||
if the model decides to. `@` is the reader saying "use this one"."""
|
||||
_note, skill, _base = _library(db)
|
||||
chat_id = make_chat()
|
||||
|
||||
client.post("/api/files/from-skill", data={"skill_id": skill.id, "chat_id": chat_id})
|
||||
|
||||
attachment = db.scalars(select(Attachment)).one()
|
||||
assert attachment.source_label == "Skill"
|
||||
assert "Steps." in attachment.extracted_text
|
||||
|
||||
|
||||
def test_a_base_is_attached_as_a_reference_not_a_copy(
|
||||
client: TestClient, db, registered, make_chat
|
||||
):
|
||||
"""Scoping, not copying. A folder of contracts in the window would cost the
|
||||
context on every request forever to answer one question."""
|
||||
from lembas.db.models import Chat
|
||||
|
||||
_note, _skill, base = _library(db)
|
||||
chat_id = make_chat()
|
||||
|
||||
response = client.post(f"/api/chats/{chat_id}/bases", data={"base_id": base.id})
|
||||
|
||||
assert response.status_code == 200
|
||||
db.expire_all()
|
||||
assert [b.id for b in db.get(Chat, chat_id).knowledge_bases] == [base.id]
|
||||
# Nothing was copied into the message.
|
||||
assert db.scalars(select(Attachment)).all() == []
|
||||
|
||||
|
||||
def test_attaching_the_same_base_twice_is_not_an_error(
|
||||
client: TestClient, db, registered, make_chat
|
||||
):
|
||||
from lembas.db.models import Chat
|
||||
|
||||
_note, _skill, base = _library(db)
|
||||
chat_id = make_chat()
|
||||
|
||||
client.post(f"/api/chats/{chat_id}/bases", data={"base_id": base.id})
|
||||
client.post(f"/api/chats/{chat_id}/bases", data={"base_id": base.id})
|
||||
|
||||
db.expire_all()
|
||||
assert len(db.get(Chat, chat_id).knowledge_bases) == 1
|
||||
|
||||
|
||||
def test_a_library_document_now_carries_its_provenance(client: TestClient, db, registered):
|
||||
"""This was the one attach path that dropped it, while a project file beside
|
||||
it carried path and machine."""
|
||||
from lembas.services.fetch import Fetched
|
||||
from lembas.services.library import documents as documents_service
|
||||
|
||||
owner = db.scalars(select(User)).first()
|
||||
base = documents_service.default_base(db, owner)
|
||||
document = documents_service.store_page(
|
||||
db,
|
||||
owner=owner,
|
||||
base=base,
|
||||
page=Fetched(url="https://tolkien.test/c", title="The Contract", text="Terms."),
|
||||
)
|
||||
|
||||
client.post("/api/files/from-knowledge", data={"document_id": document.id})
|
||||
|
||||
attachment = db.scalars(select(Attachment)).one()
|
||||
assert attachment.source_path == "The Contract"
|
||||
assert attachment.source_label == base.name
|
||||
|
||||
|
||||
# --- Attaching ---------------------------------------------------------------
|
||||
def test_a_mentioned_file_arrives_with_its_contents(client: TestClient, db, registered, box):
|
||||
profile = _profile(db, box)
|
||||
|
||||
Reference in New Issue
Block a user