Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions stacklets/docs/bot/archivist.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
)
from stack import media, resolve_model
from stack.email_message import defang_links
from stack.links import go_docs, go_topic, public
from stack.links import go_capture, go_docs, go_topic, public
from stack.ai.client import (
ModelCapabilities,
)
Expand Down Expand Up @@ -2444,7 +2444,7 @@ async def _reply_for_capture(
self.t,
source_title_hint=o.source_title_hint,
classification=o.classification,
link=o.display_link,
link=self._capture_link(o) or o.display_link,
transcript=o.transcript,
todo_link=self._todo_link(o),
)
Expand All @@ -2462,6 +2462,30 @@ async def _reply_for_capture(
)
await self._answer(room_id, reply, reply_to, metadata=metadata)

def _capture_link(self, o: CaptureOutcome) -> str:
"""A `/go/capture/<id>` link to the page this capture became.

The footer used to carry the source: the URL that was pasted, or
a placeholder for typed text. That is the message directly above
the reply in the room, so it told the sender nothing they did not
have, while the one new thing -- where it was filed -- was not
there at all. The page itself carries the source, so nothing is
lost by pointing at the page instead.

Keyed by capture id rather than vault path because the path moves
on its own: a capture re-scopes when a second person joins the
room, a topic gets renamed, a correction rewrites the title. The
resolver looks the id up in the tree it serves.

"" when there is nothing to link -- no home base yet, or a
capture that never reached the mirror and so has no page. The
caller falls back to the source footer.
"""
capture_id = ((o.envelope or {}).get("data") or {}).get("capture_id")
if not capture_id:
return ""
return public(go_capture(str(capture_id)), self.link_base_url)

def _todo_link(self, o: CaptureOutcome) -> str:
"""A `/go/topic/<scope>/todo` link when a topic capture produced todos.

Expand Down
53 changes: 53 additions & 0 deletions tests/unit/stacklets/test_archivist_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
sys.path.insert(0, str(_REPO_ROOT / "stacklets" / "docs" / "bot"))

import voice # noqa: E402
from capture_pipeline import CaptureOutcome # noqa: E402
from archivist import ArchivistBot # noqa: E402


Expand Down Expand Up @@ -1122,3 +1123,55 @@ async def test_a_capture_still_files_when_there_is_nowhere_to_keep_it(

assert calls[0]["kept_media"] is None
assert calls[0]["file_data"] == b"%PDF-1.4 anmeldung"


class TestTheReplyPointsAtThePage:
"""What the "saved" message links to.

The footer used to be the source: the URL that was pasted, or a
placeholder for typed text. That is the message directly above the
reply in the room, so it repeated what the sender already had and
left out the only new fact, which is where the capture was filed.
"""

def _outcome(self, capture_id="$abc:server"):
data = {"vault_path": "family/camping/notes/2026/09/x.md"}
if capture_id:
data["capture_id"] = capture_id
return CaptureOutcome(
status="captured",
classification={"title": "Boiler service"},
display_link="https://example.org/boiler",
envelope={"type": "capture.filed", "data": data},
)

def test_the_link_addresses_the_capture_by_id(self, tmp_path, monkeypatch):
"""Not by vault path: a capture re-scopes when a second person
joins the room, a topic gets renamed, a correction rewrites the
title. The id is assigned once and never rewritten."""
monkeypatch.setenv("LINK_BASE_URL", "https://home.example.org/go")
bot = _build_bot(tmp_path)

link = bot._capture_link(self._outcome())

assert link == "https://home.example.org/go/capture/%24abc%3Aserver"

def test_a_capture_that_never_landed_has_nothing_to_link(
self, tmp_path, monkeypatch,
):
"""No mirror, no page. The caller falls back to the source."""
monkeypatch.setenv("LINK_BASE_URL", "https://home.example.org/go")
bot = _build_bot(tmp_path)

assert bot._capture_link(self._outcome(capture_id=None)) == ""

def test_no_home_base_means_no_link_rather_than_a_broken_one(
self, tmp_path, monkeypatch,
):
"""Core renders LINK_BASE_URL; until it has, the namespace is not
reachable and posting a URL into it would be a dead link."""
monkeypatch.delenv("LINK_BASE_URL", raising=False)
bot = _build_bot(tmp_path)

assert bot._capture_link(self._outcome()) == ""

Loading