defmodule OpenAgents.Memories.SystemRecallTest do
@moduledoc """
The one recall path that crosses an account boundary: what it takes to turn
it on, what it lets through when it is on, and what it changes when it is
off.
Four properties carry the weight.
**Off is off, and off is the default.** The flag is `false` in
`config/config.exs` and declared `false` in the production and staging
profiles, and with it off an account's own recall is not merely similar to
what it was before this bucket had a recall path — it is byte for byte the
same. That is proved by comparison here rather than asserted: the same
account's recall is captured with no system store behind it, an admitted
system store is then written, and the two results are compared whole.
**Eligibility is derived, never claimed.** A row surfaces only when the
admission records derive `admitted` for it. A candidate, a rejected row, a
suspended row, a superseded row, and a row whose own `admission` column says
`admitted` with no steward behind it all reach nobody.
**The caps bound volume, not truth.** One account that wrote most of the
admitted store still fills at most its quarter of a message's pool and at
most one line of a note.
**It is deterministic.** Equal inputs give equal notes, every time.
"""
use OpenAgents.DataCase, async: false
alias OpenAgents.Memories
alias OpenAgents.Memories.{Admissions, Memory, Note, Recall, SystemRecall}
# The owner account is an operator by definition, so a steward needs no
# configuration change.
@owner_github_id 14_167_547
setup do
original = Application.get_env(:openagents, :memory_recall)
on_exit(fn -> Application.put_env(:openagents, :memory_recall, original) end)
:ok
end
defp surfacing(enabled?) do
settings =
:openagents
|> Application.get_env(:memory_recall, [])
|> Keyword.put(:system_bucket_enabled, enabled?)
Application.put_env(:openagents, :memory_recall, settings)
end
defp account(key) do
digest = :crypto.hash(:sha256, key)
github_id = digest |> binary_part(0, 7) |> :binary.decode_unsigned()
upsert(github_id, "sysrec-" <> (digest |> Base.encode16(case: :lower) |> binary_part(0, 12)))
end
defp steward, do: upsert(@owner_github_id, "AtlantisPleb")
defp upsert(github_id, login) do
{:ok, user} =
OpenAgents.Accounts.upsert_github_user(%{
github_id: github_id,
github_login: login,
github_avatar_url: "https://avatars.githubusercontent.com/u/#{github_id}?v=4"
})
user
end
defp evidence do
[%{"kind" => "receipt", "ref" => "receipt:4f1c", "digest" => "sha256:9ab3"}]
end
defp candidate(overrides \\ %{}) do
Map.merge(
%{
"bucket" => "system",
"slug" => "sys:gateway-402-retired-model",
"body" =>
"A 402 from the inference gateway usually means the default model was " <>
"retired upstream. Check gateway status before bisecting local lanes.",
"entity" => "inference-gateway",
"tier" => "ledger",
"as_of" => ~D[2026-08-25],
"admission" => "candidate",
"evidence_refs" => evidence()
},
overrides
)
end
# A row assembled as a struct rather than through `changeset/2`, so the tier
# floor is proved against the table rather than against the validation.
defp around_the_changeset(user, overrides) do
fields =
Map.merge(
%{
user_id: user.id,
bucket: "system",
body: "Written around the write path.",
slug: "sys:around-the-write-path",
tier: "ledger",
as_of: ~D[2026-08-25],
admission: "candidate",
evidence_refs: evidence()
},
overrides
)
Repo.insert(struct(Memory, fields))
end
# Written, then judged. The verdict is a steward's record, which is the only
# thing `status/1` reads.
defp admitted(author, overrides \\ %{}) do
{:ok, memory} = Memories.create(author, candidate(overrides))
{:ok, _record} =
Admissions.record(steward(), memory.id, %{
"verdict" => "admitted",
"ground" => "The receipt supports the claim."
})
memory
end
@turn "the inference gateway returned 402 on the default model"
describe "the flag, off" do
test "is the default this repository ships" do
refute SystemRecall.enabled?()
end
test "surfaces an admitted system memory to nobody, including its author" do
author = account("off-author")
reader = account("off-reader")
memory = admitted(author)
assert Admissions.status(memory) == "admitted"
assert %Recall{memories: []} = Memories.recall(reader, @turn)
assert %Recall{memories: []} = Memories.recall(author, @turn)
end
test "issues no query for the bucket at all" do
assert SystemRecall.pool(@turn) == []
end
test "leaves user and learned recall byte for byte what it was" do
reader = account("off-identical")
author = account("off-identical-author")
{:ok, _asked} = Memories.create(reader, %{"body" => "I use pnpm, not npm."})
{:ok, _learned} =
Memories.create(reader, %{
"body" => "The inference gateway returned 402 during the last deploy.",
"bucket" => "learned"
})
before = Memories.recall(reader, @turn)
before_note = Note.render(before)
# An admitted store, written by another account and by this one, is now
# behind the same query.
_theirs = admitted(author)
_mine = admitted(reader, %{"slug" => "sys:precommit-installs-the-push-guard"})
after_store = Memories.recall(reader, @turn)
assert after_store == before
assert Note.render(after_store) == before_note
refute before_note == nil
end
end
describe "the flag, on" do
setup do
surfacing(true)
:ok
end
test "an admitted ledger memory reaches an account that did not write it" do
author = account("on-author")
reader = account("on-reader")
memory = admitted(author)
%Recall{memories: recalled} = Memories.recall(reader, @turn)
assert Enum.map(recalled, & &1.id) == [memory.id]
end
test "and reaches its own author too" do
author = account("on-own-author")
memory = admitted(author)
%Recall{memories: recalled} = Memories.recall(author, @turn)
assert Enum.map(recalled, & &1.id) == [memory.id]
end
test "a candidate with no verdict behind it reaches nobody" do
author = account("on-candidate")
reader = account("on-candidate-reader")
{:ok, memory} = Memories.create(author, candidate())
assert Admissions.status(memory) == "candidate"
assert %Recall{memories: []} = Memories.recall(reader, @turn)
end
test "nor does a row that claims admission for itself" do
author = account("on-self-claimed")
reader = account("on-self-claimed-reader")
{:ok, memory} = Memories.create(author, candidate(%{"admission" => "admitted"}))
assert memory.admission == "admitted"
assert Admissions.status(memory) == "candidate"
assert %Recall{memories: []} = Memories.recall(reader, @turn)
end
test "a rejected memory reaches nobody" do
author = account("on-rejected")
reader = account("on-rejected-reader")
{:ok, memory} = Memories.create(author, candidate())
{:ok, _record} =
Admissions.record(steward(), memory.id, %{
"verdict" => "rejected",
"ground" => "The receipt does not say this."
})
assert Admissions.status(memory) == "rejected"
assert %Recall{memories: []} = Memories.recall(reader, @turn)
end
test "a suspended memory leaves recall until the challenge is resolved" do
author = account("on-suspended")
reader = account("on-suspended-reader")
challenger = account("on-suspended-challenger")
memory = admitted(author)
{:ok, challenge} =
Admissions.challenge(challenger, memory.id, %{
"ground" => "The gateway returns 402 for an expired credential too.",
"evidence_refs" => [
%{"kind" => "url", "ref" => "https://example.test/status", "digest" => "sha256:1c2d"}
]
})
assert Admissions.status(memory) == "suspended"
assert %Recall{memories: []} = Memories.recall(reader, @turn)
{:ok, _refutation} =
Admissions.refute(steward(), challenge.id, %{
"ground" => "The receipt distinguishes the two cases."
})
assert Admissions.status(memory) == "admitted"
%Recall{memories: restored} = Memories.recall(reader, @turn)
assert Enum.map(restored, & &1.id) == [memory.id]
end
test "a superseded memory reaches nobody; its replacement does" do
author = account("on-superseded")
reader = account("on-superseded-reader")
memory = admitted(author)
{:ok, replacement} =
Admissions.supersede(
author,
memory.id,
candidate(%{"body" => "A 402 means the credential expired. Check the key first."})
)
{:ok, _record} =
Admissions.record(steward(), replacement.id, %{
"verdict" => "admitted",
"ground" => "The corrected claim is supported."
})
%Recall{memories: recalled} = Memories.recall(reader, @turn)
assert Enum.map(recalled, & &1.id) == [replacement.id]
end
# The tier floor is enforced in three places and reachable through none of
# them. `dark` and `pulse` are refused by the changeset, refused by
# `memories_system_shape` when a caller writes around it, and named as a
# predicate in the eligibility read so the floor does not depend on the
# table alone.
test "a sub-ledger tier cannot be written, and the read names the floor anyway" do
author = account("on-dark-tier")
assert {:error, changeset} = Memories.create(author, candidate(%{"tier" => "dark"}))
assert %{tier: ["is invalid"]} = errors_on(changeset)
assert_raise Ecto.ConstraintError, ~r/memories_system_shape/, fn ->
around_the_changeset(author, %{tier: "pulse"})
end
assert SystemRecall.tiers() == ~w(ledger glass)
end
test "an empty store recalls nothing rather than failing the turn" do
reader = account("on-empty")
assert %Recall{memories: [], dropped: 0} = Memories.recall(reader, @turn)
end
end
describe "the note" do
setup do
surfacing(true)
:ok
end
test "names the bucket, the claim date, and the derived status" do
author = account("note-author")
reader = account("note-reader")
_memory =
admitted(author, %{"body" => "Check gateway status first.", "as_of" => ~D[2026-08-25]})
note = reader |> Memories.recall(@turn) |> Note.render()
assert note ==
"[From memory: (system, as of 2026-08-25, admitted)] Check gateway status first."
end
test "reads differently from the account's own memories in the same note" do
reader = account("note-mixed")
author = account("note-mixed-author")
{:ok, _asked} = Memories.create(reader, %{"body" => "I use pnpm, not npm."})
_memory = admitted(author, %{"body" => "Check gateway status first."})
note = reader |> Memories.recall(@turn) |> Note.render()
assert [own, network] = String.split(note, "\n")
assert own =~ ~r/^\[From memory: user, /
assert network ==
"[From memory: (system, as of 2026-08-25, admitted)] Check gateway status first."
end
test "prints the status the records derived rather than the column the author wrote" do
author = account("note-derived")
reader = account("note-derived-reader")
_memory =
admitted(author, %{"admission" => "rejected", "body" => "Check gateway status first."})
note = reader |> Memories.recall(@turn) |> Note.render()
assert note =~ "admitted)"
refute note =~ "rejected"
end
test "spends the account's character budget rather than one of its own" do
reader = account("note-budget")
author = account("note-budget-author")
{:ok, _asked} = Memories.create(reader, %{"body" => String.duplicate("a", 60)})
_memory = admitted(author, %{"body" => String.duplicate("b", 60)})
%Recall{memories: recalled} =
Memories.recall(reader, @turn, maximum_attached_characters: 80)
assert length(recalled) == 1
assert hd(recalled).bucket == "user"
end
end
describe "the caps" do
setup do
surfacing(true)
:ok
end
test "hold the pool to a quarter of its slots per account" do
prolific = account("cap-prolific")
other = account("cap-other")
for index <- 1..8 do
admitted(prolific, %{
"slug" => "sys:prolific-#{index}",
"body" => "Gateway claim number #{index} about the inference gateway."
})
end
only = admitted(other, %{"slug" => "sys:other-1", "body" => "One claim about the gateway."})
pool = SystemRecall.pool(@turn)
# Nine eligible rows, so the cap is three per account, rounded up.
assert SystemRecall.pool_cap(9) == 3
assert Enum.count(pool, &(&1.user_id == prolific.id)) == 3
assert only.id in Enum.map(pool, & &1.id)
end
test "hold a note to one memory per account and two in all" do
prolific = account("note-cap-prolific")
second = account("note-cap-second")
third = account("note-cap-third")
reader = account("note-cap-reader")
for index <- 1..4 do
admitted(prolific, %{
"slug" => "sys:note-prolific-#{index}",
"body" => "Gateway claim number #{index} about the inference gateway."
})
end
admitted(second, %{
"slug" => "sys:note-second",
"body" => "A second account's gateway claim."
})
admitted(third, %{"slug" => "sys:note-third", "body" => "A third account's gateway claim."})
%Recall{memories: recalled} = Memories.recall(reader, @turn)
assert length(recalled) == 2
authors = Enum.map(recalled, & &1.user_id)
assert authors == Enum.uniq(authors)
assert Enum.count(recalled, &(&1.user_id == prolific.id)) <= 1
end
test "leave a single-account store recallable rather than empty" do
author = account("cap-single")
reader = account("cap-single-reader")
for index <- 1..4 do
admitted(author, %{
"slug" => "sys:single-#{index}",
"body" => "Gateway claim number #{index} about the inference gateway."
})
end
%Recall{memories: recalled} = Memories.recall(reader, @turn)
assert length(recalled) == 1
end
test "do not report what they excluded" do
prolific = account("cap-silent")
reader = account("cap-silent-reader")
for index <- 1..4 do
admitted(prolific, %{
"slug" => "sys:silent-#{index}",
"body" => "Gateway claim number #{index} about the inference gateway."
})
end
%Recall{dropped: dropped} = Memories.recall(reader, @turn)
assert dropped == 0
end
test "round up so a pool of one is not emptied by its own cap" do
assert SystemRecall.pool_cap(0) == 0
assert SystemRecall.pool_cap(1) == 1
assert SystemRecall.pool_cap(4) == 1
assert SystemRecall.pool_cap(5) == 2
assert SystemRecall.pool_cap(8) == 2
end
end
describe "determinism" do
setup do
surfacing(true)
:ok
end
test "equal inputs give equal pools and equal notes" do
first = account("det-first")
second = account("det-second")
reader = account("det-reader")
for index <- 1..3 do
admitted(first, %{
"slug" => "sys:det-first-#{index}",
"body" => "First account's gateway claim number #{index}."
})
admitted(second, %{
"slug" => "sys:det-second-#{index}",
"body" => "Second account's gateway claim number #{index}."
})
end
pools = for _repeat <- 1..5, do: Enum.map(SystemRecall.pool(@turn), & &1.id)
notes = for _repeat <- 1..5, do: reader |> Memories.recall(@turn) |> Note.render()
assert Enum.uniq(pools) == [hd(pools)]
assert Enum.uniq(notes) == [hd(notes)]
end
end
# MEMORY-004 and MEMORY-001's amendment. The eligibility filter is what
# replaces the scope predicate for this bucket, so it has to be a predicate:
# written into the query, not applied to what the query returned.
describe "MEMORY-004" do
@source "lib/openagents/memories/system_recall.ex"
test "the eligibility filter is written into the query" do
query = @source |> queries() |> List.first()
assert query, "no query rooted at `Memory` found in #{@source}"
for predicate <- ~w(bucket superseded_by_id tier id)a do
assert names?(query, predicate),
"""
The eligibility read in #{@source} does not name `#{predicate}`.
MEMORY-001's amendment replaces the account predicate with this
filter, so every part of it belongs in the query.
#{Macro.to_string(query)}
"""
end
end
test "and it names no account" do
for query <- queries(@source) do
refute names?(query, :user_id),
"""
A query in #{@source} names `user_id`. This module holds the
reads that deliberately do not; an account-scoped read belongs
in `OpenAgents.Memories`.
"""
end
end
defp queries(path) do
path
|> File.read!()
|> Code.string_to_quoted!()
|> Macro.prewalker()
|> Enum.filter(fn
{:from, _meta, [{:in, _, [_binding, {:__aliases__, _, [:Memory]}]} | _]} -> true
_other -> false
end)
end
defp names?(query, field) do
query
|> Macro.prewalker()
|> Enum.any?(fn
{{:., _, [_, ^field]}, _, _} -> true
_other -> false
end)
end
end
# The bucket still belongs to the account that wrote it for every purpose but
# recall. Reading a network claim in a turn does not make it listable,
# fetchable, or correctable by the reader.
describe "the write boundary is unmoved" do
setup do
surfacing(true)
:ok
end
test "a reader who recalls a system memory still cannot read it through the store" do
author = account("boundary-author")
reader = account("boundary-reader")
memory = admitted(author)
%Recall{memories: recalled} = Memories.recall(reader, @turn)
assert Enum.map(recalled, & &1.id) == [memory.id]
assert {:error, :not_found} = Memories.fetch(reader, memory.id)
assert Memories.list(reader, bucket: "system") == []
assert {:error, :not_supersedable} = Admissions.supersede(reader, memory.id, candidate())
end
test "and the account's own buckets stay account-scoped with the flag on" do
reader = account("boundary-scoped")
other = account("boundary-scoped-other")
{:ok, _theirs} = Memories.create(other, %{"body" => "Deploy with yarn."})
assert %Recall{memories: []} = Memories.recall(reader, "deploy with yarn")
end
end
end