Skip to content

feat(agora): channel identity, authority, and delivery hardening - #7151

Draft
forkwright wants to merge 45 commits into
mainfrom
fix/agora-channel-hardening-local
Draft

feat(agora): channel identity, authority, and delivery hardening#7151
forkwright wants to merge 45 commits into
mainfrom
fix/agora-channel-hardening-local

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Recovered from a local-only branch on metis during a box lean-down: 45 commits, 71 files, +12,764/-1,993, never pushed and never PR'd. Opening as a draft so the work is discoverable and has a lifecycle rather than living as a bare ref.

What it is

Agora messaging-channel identity and authority hardening:

  • account-id propagation and participant allowlists
  • bang-command policy gating, typed command dispatch isolation
  • durable message IDs with ingress dedup
  • inbound concurrency caps
  • channel-identity redaction
  • Matrix cursor fail-closed handling
  • listener lifecycle observability
  • Signal delivery hardening

The commit tail is CI cleanup (clippy pedantic, dead-code, disallowed-method, two miscounted-test fixes), so it was being driven toward a clean gate rather than abandoned mid-thought.

Provenance — this supersedes several dead refs

It is the superset of the whole #7003 effort. Verified by patch-id and, where patch-ids disagreed, by direct file diff (the only deltas were an .iter().any().contains() refactor and doc-comment reordering — later polish on identical logic):

Once this lands, those eight refs can be deleted with no loss.

State — read before reviewing

The tip is from 2026-08-28 and main has moved a long way since (PRs #7037#7106 landed in that window). This needs a rebase and a fresh gate before it is mergeable; it is draft for exactly that reason. Nothing here has been verified against current main — the claim being made is only that the work exists and is unlanded, not that it is ready.

Cody Kickertz and others added 30 commits August 26, 2026 12:56
InboundMessage had no receiving-account identity, so multi-account
Signal/Matrix deployments collapsed identical senders and rooms across
accounts onto one route, and replies left from the provider's default
account instead of the account that received the message.

- Add account_id to InboundMessage; both providers stamp it during
  extraction.
- Signal's poll loop now passes the account to signal-cli receive
  instead of None, so multi-account daemons are polled per account.
- ChannelBinding gains an optional account leg: a scoped binding
  matches only messages that account received; unscoped bindings keep
  matching any account. Session-key templates gain an {account}
  placeholder.
- Replies send with the inbound account_id, and command audit records
  and dispatch spans carry it.

Part of #5195
Group routing matched on channel + group_id alone, so every participant
of a configured group could drive the agent and whatever command surface
the route permitted, even when the operator intended one trusted
participant.

ChannelBinding gains a participants allowlist: when non-empty, only the
listed senders may activate the binding; other senders — including
fellow group members — fall through to lower-priority routes. The leg
applies uniformly to group, source, and channel-wildcard bindings, so a
wildcard with participants doubles as a per-sender channel allowlist.
Empty preserves the previous any-participant behavior.

Part of #5194
Any inbound message that parses as a ! command was intercepted after
route resolution and could enumerate agents, channel health, models,
skills, blackboard entries, and sessions. Wildcard and default route
bindings — the documented quickstart posture — exposed that surface to
every sender.

Add MessagingConfig.commands (InboundCommandPolicy), fail-closed by
default: only public_commands (help, ping) are reachable, and the
operator surface requires an explicit operators entry naming the
channel+sender (channel:source, per-segment wildcard). default_allow
restores the old behavior as an explicit opt-out. Denied commands get a
refusal reply, a warning, and an aletheia_command_denied_total metric;
!help shows non-operator senders only the public subset. Unknown
commands stay public because their reply leaks nothing beyond the
filtered !help hint.

The allowlist contents are operator policy; this commit ships only the
enforcement substrate and the deny default.

Part of #5193
The channel ingress model had no stable provider message IDs and no
durable checkpoints: restarts and retry windows could replay or
duplicate messages, Matrix parsed event_id but dropped it, and every
outbound send used a fresh UUID so retried replies could double-post.

- InboundMessage gains message_id (Matrix event_id; signal-cli exposes
  none) and a dedupe_key() that prefers the provider ID and falls back
  to a SHA-256 content hash over the routing identity.
- A bounded DedupeFilter (agora::dedupe, 4096-key FIFO window) runs in
  the dispatcher before any routing or agent work; repeats are dropped
  and counted in aletheia_ingress_duplicates_total.
- SendParams gains an idempotency_key; Matrix maps it to the room-send
  transaction ID, and dispatch replies derive it from the inbound
  message identity, so a replayed inbound event cannot double-post.
- Matrix sync cursors persist per account through a CursorStore
  (JSON-file implementation under the instance data dir, hashed file
  names so account IDs stay out of paths); a persisted cursor overrides
  initial_since on startup, and each processed batch checkpoints
  (aletheia_cursor_checkpoints_total). Signal needs no cursor —
  signal-cli receive consumes destructively.

The full ingress journal from the issue is deliberately not in this
commit; the persisted cursor plus bounded dedupe window cover the
restart/retry replay paths without a second storage engine.

Part of #5196
messaging.maxConcurrentHandlers was enforced only inside
ChannelListener::run, but the runtime consumed the receiver via
into_receiver and spawned one dispatch task per message — the cap was
dead config, and a message flood could create unbounded dispatch and
LLM work. into_receiver also orphaned the provider poll handles at the
end of the setup block.

spawn_dispatcher now owns the ChannelListener and dispatches through
listener.run(), so the configured cap bounds in-flight work on the live
path and the provider handles drain with the listener at shutdown. run()
gains saturation instrumentation: an in-flight gauge
(aletheia_inbound_handlers_in_flight) and a counter
(aletheia_inbound_handler_saturation_total) incremented whenever the
receive loop has to wait for a free slot, which is the backpressure
signal — upstream mpsc buffers fill and provider poll loops suspend.

A flood test drives four messages at a cap of two and pins that handler
concurrency reaches but never exceeds the limit.

Per-channel rate limits from the issue are not in this commit; the
bounded channels already exert backpressure and the saturation metric
makes pressure visible.

Part of #5197
Channel payloads and sender identifiers were inconsistently redacted:
raw Signal envelopes and Matrix events were captured unconditionally,
dispatch spans and warnings logged full senders, command audit records
persisted raw sender/group IDs, Signal health payloads keyed on raw
phone numbers, and the only redaction helper was a Signal-specific
redact_phone used in exactly one span.

- agora::redact centralizes channel-identity redaction (suffix-4 form)
  and is now used by the listener span, the dispatch span and no-route
  warning, the Signal poll span, command audit origin records, and the
  Signal probe/connection-health payloads.
- messaging.retainRawPayloads (default false) gates raw payload capture
  in both providers' extraction paths; the raw envelope/event is
  attached only on explicit operator opt-in.
- InboundMessage loses its derived Debug for a manual impl that redacts
  identities, reports text/attachments by size only, and shows a
  captured raw payload as present-or-absent.

Tests cover phone/Matrix-ID/short-value redaction, the Debug output
contract, raw-payload opt-in and opt-out on both extractors, redacted
health keys, and raw identifiers staying out of command audit history.

Part of #5198
ChannelListener set the active-subscriptions gauge at construction but
cleared it only on a drop path that into_receiver disabled, and the
runtime called into_receiver immediately — so the gauge pinned at the
startup count for the life of the process regardless of actual provider
task state.

Gauge ownership now lives in a SubscriptionGuard: construction adds the
handle count, guard drop subtracts it. run() holds the guard until every
handler and provider task has drained; into_receiver returns the guard
with the receiver and handles so manual-control callers keep the metric
honest. The set-to-zero drop heuristic is gone, so two overlapping
listeners no longer stomp each other's count.

Tests cover the transfer (gauge survives into_receiver, clears on guard
drop) and the existing drop/run shutdown paths.

Part of #5204
…hema

The CONFIGURATION.md field reference is generated from the taxis schema
doc comments (aletheia#5774); the cursor-precedence note on
initialSince, the saturation-metrics note on maxConcurrentHandlers, and
the messaging.commands posture prose belong in the source doc comments
so the generated table carries them.

Part of #5196, part of #5197
Two CI-gate hazards in the new modules: the dedupe test module carried
an expect(clippy::expect_used) attribute with no expect call to fulfill
(unfulfilled_lint_expectations is denied), and types::hex_lower indexed
a byte string, which the workspace's indexing_slicing warn-deny rejects.
hex_digit now matches on the nibble like the existing dispatch helper.

Part of #5196
The agora crate gained a sha2 workspace dependency for the dedupe
content hash; the lockfile edge travels with it since cargo cannot run
in the authoring environment.

Part of #5196
WHY: the redaction commit passed the loop-shadowed owned account_id
String to redact::identifier(&str) (E0308), and the concurrency-cap
flood test moved the max_seen/completed Arcs into the run() closure
and then asserted on them after the move (E0382). Local cargo check
is unavailable in this lane, so both surfaced only in the CI gate:
borrow the account id, and clone the two asserted Arcs before the
closure so the originals remain for the post-run assertions.
Move active-subscription ownership into each Matrix account future so normal exit, cancellation, and task failure release the gauge through RAII. This follows the listener lifecycle change that removes listener-owned subscription accounting.
`toml::map::Map` has no `values_mut`. It carries `iter_mut` and `get_mut`
(toml 0.8.23, `src/map.rs:107,214`), which is what the pre-existing
`encrypt_recursive` in this same file already uses.

That single unresolved method produced all five gate errors: once `entry` binds
to an error type, `as_table_mut`, `auth.get_mut(field)`, and the `*secret =
encrypt_value(...)?` assignment each fail in turn, reported as E0308 x3 and E0277
on lines that are themselves correct and identical in shape to the working call
site at `encrypt.rs:422`.

Verified this was the only `values_mut` the branch introduced; the other call
sites in the workspace are `HashMap`/`serde_json::Map`, which do have it.
Cody Kickertz added 15 commits August 28, 2026 11:51
…ss borrow

clippy -D warnings failed gate-coverage-compile-checks on three lints in
fjall_store.rs introduced by the command_lifecycle feature: delete_session
and put_command_lifecycle_record_in_tx exceeded too_many_lines (135/100 and
107/100), and validate_command_lifecycle_record took a needless reference
to an already-&str field.

Extracted the command_lifecycle key-collection/index-check block out of
delete_session into command_lifecycle_delete_keys_for_session_in_tx
(mirroring the existing tool_audit_keys_for_session_in_tx /
note_gid_delete_keys_for_session_in_tx helpers), and factored the repeated
prefix-range scans (messages/usage/distillations/notes) into
prefixed_child_keys_in_tx. Split put_command_lifecycle_record_in_tx into a
session/id/record-building half (build_command_lifecycle_record_in_tx) and
the collision-check/insert half. Dropped the redundant & on
spec.delivery_key in is_digest.
…me errors

crates/aletheia/src/dispatch.rs: call get_history with session_id.as_str()
instead of &StoredSessionId -- StoredSessionId no longer derefs to &str
since 3d6b28e typed command lifecycle owner ids, and get_history expects
&str.

crates/agora/src/listener.rs: three test ChannelProvider impls
(PartiallyFailedProvider, CooperativeCancellationProvider,
UnexpectedCancellationProvider) had name() delegate to self.id(). Calling
a trait method through self ties the return lifetime to the trait's
declared (elided, &self-bound) signature rather than the impl's narrower
&'static str override, so the borrow checker cannot prove the result
outlives 'static. Match the repo's own idiom (TestProvider, PanicProvider)
and return the literal directly instead of delegating.
…b baseline

- crates/pylon/src/tests/error.rs: removed four deep_merge_* tests whose
  target (handlers::config::deep_merge) was deleted when config.rs moved
  patch-merge logic into taxis::redact::apply_section_patch_with_marker_authority;
  equivalent coverage already exists in crates/taxis/src/redact.rs.
- crates/taxis/src/redact.rs: fixed E0308 in marker_literal_at_non_sensitive_path_remains_data
  -- EmbeddingSettings.model is Option<String>, compared via .as_deref().
- crates/graphe/src/store/fjall_store.rs: removed unused fjall::Readable
  import in delete_session (no trait method of it is called in that fn).
- scripts/stub-baseline.toml: regenerated with --write-baseline -- this
  branch's agora/semeion/client.rs changes paid down the id/jsonrpc stub
  sites the baseline still claimed (count 1, tree has 0).
full-gate-build failed with 4 dead_code errors under -D warnings:
MatrixEvent::unsigned/extra, MatrixEventContent::msgtype, and
EnvelopeOutcome::UnsupportedContentLost/MalformedLost's kind/reason
fields, all deserialized/constructed but never read.

The Signal envelope fields were the real defect: normalize_batch
discarded kind and reason via '..' after only reading lost_parts,
throwing away the exact diagnostic content this hardening effort
exists to preserve. It now logs them at debug level before folding
lost_parts into the loss counter.

The Matrix fields (unsigned, top-level extra, msgtype) are genuinely
unused today -- unsigned/extra are held for the deferred raw-payload
capture path already noted at the 'raw: None' call site (#5198), and
msgtype is deserialized for shape parity but not yet used to filter
message kinds. Marked #[expect(dead_code)] with that reasoning rather
than deleted, since removing them would silently drop data the
governed capture path (#5198) still needs to deserialize.
Adding `command_lifecycle_part` put `import_bundle_children_in_tx` at 8
parameters against clippy's ceiling of 7:

    error: this function has too many arguments (8/7)
      --> crates/graphe/src/store/fjall_store.rs:4748:5

A `#[expect(clippy::too_many_arguments)]` would have cleared it, but this file
already has the better answer and graphe carries no such suppression anywhere.
`NoteTxParts` and `CommandLifecycleTxParts` group transaction keyspaces exactly
this way -- and this function was already building both of them inline out of its
own loose parameters, which is the tell that the grouping belonged one level up.

`ImportBundleTxParts` carries the six partitions an imported bundle's child rows
are written to. The signature goes 8 -> 3, the two inner structs are now built
from its fields rather than from separate arguments, and the single call site
passes one value.

Verified: `cargo clippy -p graphe --all-targets --keep-going` exit 0, no warnings.
…dening-local

# Conflicts:
#	crates/taxis/src/registry.rs
#	crates/taxis/src/reload.rs
…ption

`crates/agora/clippy.toml` disallows `std::fs::read` and `std::fs::File::open`,
offering two ways out: use `tokio::fs`, or "abstract behind a trait for
testability". The second is what this file already does -- `CursorStore` is the
trait, `FileCursorStore` is one implementation, and every consumer holds it as
`Arc<dyn CursorStore>`.

The trait is synchronous, so moving to `tokio::fs` would make every caller async
for no behavioural gain, and the directory fsync in `sync_directory` has no
`tokio::fs` equivalent at all.

Both suppressions name which of the policy's two conditions they satisfy, rather
than asserting the lint is wrong.

What this deliberately does NOT settle: the crate header says all persistence
belongs in mneme, which argues a cursor store should not live in agora at all.
That is a placement decision rather than a lint one, and it is raised on the PR
instead of being decided by an attribute.
- taxis/validate.rs:918 — validate_binding_source_kind's first let-else
  (binding.get("sourceKind") else { return None }) returns None unchanged,
  so it rewrites faithfully to the ? operator per the lint's own suggestion.
  The second let-else in the same function pushes an error before returning
  and is untouched — not what clippy flagged.
- graphe/portability.rs:408 — sample_agent_file() (test fixture builder, not
  production code) was 117 lines. Extracted sample_workspace(),
  sample_command_lifecycle_record(), and sample_session() as sub-builders;
  sample_agent_file() now composes them. Same AgentFile is produced, so no
  assertion in any consumer of the fixture changes.
- graphe/store/fjall_store_tests.rs:1191 —
  delete_session_removes_usage_distillation_and_note_rows is one linear
  scenario (populate every child partition, delete, assert all partitions
  empty). Suppressed with #[expect(clippy::too_many_lines, reason = ...)]
  per the repo's own idiom (crates/aletheia/src/commands/agent_io.rs);
  splitting it would obscure the single guarantee under test.
- graphe/store/fjall_store_tests_portability.rs:624 —
  import_session_bundle_writes_everything_atomically is the same shape (one
  atomic bundle import, assert every partition landed). Same #[expect]
  treatment, same rationale.

No test assertions changed.
…ailures

Four independent CI failures on run 33210347451 (head 97cea15), all fixed
in this pass.

crates/agora/src/matrix/mod.rs:389 -- unnecessary_sort_by. accounts.sort_unstable_by
compared keys by reference (`left.cmp(right)`); the account id key is
`String` (not Copy), so clippy's own `*left` suggestion does not type-check.
Rewritten as `sort_unstable_by_key(|(left, _)| (*left).clone())` -- pays one
clone per account (a probe path, not hot) rather than keeping a manual
comparator clippy flags as equivalent to the key-extraction form.

crates/agora/src/matrix/mod.rs:421 -- missing_fields_in_debug. The manual
`Debug for MatrixProvider` omitted `cursor_store` entirely. It is
`Option<Arc<dyn CursorStore>>` and `CursorStore` carries no `Debug` bound, so
the field itself cannot be printed. Added a derived `cursor_store_set` field
(present/absent, matching the existing `default_account_set` pattern) and
switched `.finish()` to `.finish_non_exhaustive()` to mark the repr as
deliberately partial.

crates/agora/src/matrix/mod.rs:108 -- MatrixEventContent::msgtype. Carried
`reason="deserialized for shape parity; not yet used to filter message
kinds"` with no tracking issue, so check-stub-accountability.py rejected it
as new unaccounted debt (aletheia#7082 was filed for this site). Nothing
reads `.msgtype` anywhere in the crate -- removed the field; the raw JSON key
still round-trips through the adjacent `#[serde(flatten)] extra` map, so no
deserialization behavior changes.

crates/agora/src/router.rs:297 -- manual_contains. `participant_allowed`
compared `participants.iter().any(|p| *p == msg.sender)`; `participants` is
`Vec<String>`, so the equality-search closure is exactly what `.contains()`
already does. Rewritten as `binding.participants.contains(&msg.sender)`,
same truth table, fewer lines.

crates/agora/src/semeion/client.rs:431 -- nonminimal_bool on a negated
`is_some_and`. `!response.get("timestamp").and_then(as_u64).is_some_and(|t|
t > 0)` is true exactly when the timestamp is absent, unparseable, or zero;
`is_none_or(|t| t == 0)` covers the same two cases (None, or Some via the
predicate) without the leading negation. u64 can't be negative so `t == 0`
is equivalent to `!(t > 0)`.

crates/agora/src/types.rs:217 -- single_match/single_match_else.
`InboundMessage::dedupe_key`'s two-arm `match self.group_id.as_deref() {
Some(group) => .., None => .. }` destructures only the `Some` arm;
rewritten as `if let Some(group) = .. { .. } else { .. }`, same hashing
order and same two branches.

crates/graphe/src/store/fjall_store.rs:761 -- dead_code under the ML-feature
build (gliner, nuextract). `ImportBundleTxParts` has exactly one
construction site (`import_bundle_children_in_tx`'s caller,
`import_session_bundle`), and both live inside the
`#[cfg(feature = "portability")] impl SessionStore { .. }` block spanning
lines 4295-4964. The ML-feature build config does not enable `portability`,
so that whole impl compiles out and the struct's only constructor
disappears -- while the struct definition, carrying no cfg gate of its own,
still compiles in and trips `-D warnings` on zero constructions. Gated the
struct definition with the matching `#[cfg(feature = "portability")]` so it
exists exactly when its constructor does; compared against every sibling
struct in the same file (`ImportSessionOutcome`, `ImportSessionBundle`,
`ImportSessionNote`, `ImportCommandLifecycleRecord`,
`ImportSessionWorkingState`, `ImportSessionBundleResult`), which already
carry the identical gate for the identical reason. `NoteTxParts` and
`CommandLifecycleTxParts`, which sit next to `ImportBundleTxParts` in the
default build, are untouched: both have other construction sites outside
the portability block, so they stay live in the default build.

The fourth reported failure -- "ERROR: diaporeia MCP inventory ... is out of
date" in gate-coverage-scripts -- is not a real check failure. It is the
`test-diaporeia-mcp-inventory.py` unit suite's own `test_check_mode`
deliberately exercising the --check failure path against a synthetic stale
fixture; the step prints that line and then "OK: all MCP inventory
generator tests passed" and exits 0, confirmed against the actual job log
for run 33210347451. `generate-diaporeia-mcp-inventory.py --check` against
the real crates/diaporeia/CLAUDE.md passes with no diaporeia files touched
in this branch or in origin/main ahead of it; the job's only real failure
was check-stub-accountability.py, fixed above.
…ailures

- cursor.rs: scope std::fs::write in a test fixture with an
  #[expect(clippy::disallowed_methods)], matching the repo's own
  precedent (integration-tests/tests/r722_substrate_canary.rs) for
  test setup that deliberately writes fixture bytes synchronously.
- listener.rs: move the NotifyOnDrop struct+impl before the channel
  setup in listener_drop_aborts_owned_tasks_promptly, clearing
  items_after_statements without changing behavior.
- matrix/client.rs: rewrite two match-with-panic-arm blocks as
  let...else per clippy's manual_let_else, in
  sync_rejects_missing_null_and_empty_next_batch and
  sync_does_not_follow_redirects.
- metrics.rs: add #[expect(clippy::expect_used, reason = "test
  assertions")] on the tests module, matching the same pattern
  already used in cursor.rs, listener.rs, and router.rs — this test
  module holds only assertion .expect()/.expect_err() calls.
- semeion/envelope.rs: drop the now-unfulfilled
  #[expect(clippy::indexing_slicing)] on the tests module; the tests
  use serde_json::json! + .get(), not raw indexing, so nothing in
  the module triggers the lint anymore.

gate-coverage-sovereign-hnsw is the known intermittent
(#6952); this branch touches only
crates/krites/CAPABILITY_MATRIX.toml call-site counts, no HNSW code.
clippy::doc_markdown flagged both doc lines: "Closed OpenAPI shape..."
reads as prose containing a term that looks like code and must be
marked as such. The crate already carries this exact convention
throughout openapi.rs and health.rs (`OpenAPI`), so this follows
existing house style rather than inventing a new form.

Before: /// Closed OpenAPI shape for a binding's source selector.
After:  /// Closed `OpenAPI` shape for a binding's source selector.
(and the CommandTier variant's doc line, same fix)
…heir tests

Four clippy failures in taxis's own test modules, none touching production code:

- loader.rs:969 used std::fs::write directly to corrupt a test fixture key,
  tripping the crate's disallowed_methods policy (async/testability). Scoped
  an #[expect(clippy::disallowed_methods, ...)] to the single statement,
  matching the identical precedent already in oikos.rs::check_writable and
  validate_tests.rs.
- redact.rs's mod tests block suppressed indexing_slicing but never
  expect_used, so four expect_err() calls added to that module (clippy's
  expect_used lint also fires on Result::expect_err, not only expect) were
  bare errors under -D warnings. Added the crate's standard
  #[expect(clippy::expect_used, reason = "test assertions")] alongside the
  existing indexing_slicing suppression.
- validate_tests.rs's file-level #![expect] list carried unwrap_used and
  indexing_slicing but not expect_used, so two expect_err() calls in tests
  added there were likewise bare. Added the matching file-level suppression.
- reload.rs's mod tests block had no indexing_slicing suppression at all;
  two new assertions index vec[0] immediately after asserting bindings.len(),
  the exact house pattern already suppressed in validate_tests.rs. Added the
  same #[expect] with the same reasoning.

No assertion, comparison, or expected value changed in any of the four
files — only lint scope.
…ring

Three pedantic clippy failures in crates/aletheia, none behavioral:

- dispatch_one had grown to 122 lines (threshold 100). Split the turn-send
  tail (resolve the nous handle, log, build the ingress marker, send the
  turn, reply) into a new dispatch_turn helper, leaving dispatch_one as the
  routing/dedupe/command-interception path it already reads as. No control
  flow changed; the split point is the existing boundary between 'decide
  what to do with this message' and 'do the turn'.
- command_conversation_id matched msg.group_id.as_deref() with two
  non-trivial arms (Some/None) purely to pick which two hash_field calls to
  make; clippy flagged it as destructuring a single pattern. Rewrote as
  if let ... else, per the lint's own suggestion — same three hashed
  fields, same order, same values.
- start_inbound_dispatch took 8 arguments against this crate's local
  clippy.toml threshold of 7 (unset there, so it falls back to clippy's
  default rather than the workspace root's raised threshold of 10). Bundled
  the seven wiring inputs (config, nous_manager, session_store, ready_rx,
  the two optional channel providers, shutdown_token) into an
  InboundDispatchDeps struct, matching the DispatcherParts pattern already
  used two lines later in the same function for the same reason. Updated
  the sole caller in runtime/mod.rs accordingly.

No timing, ordering, or error handling changed in any of the three sites.
…ction claim

The nextest run this PR's compile-blocking clippy fixes finally let through
surfaced a genuine (pre-existing, not lint-related) test bug: after
capacity 2 evicts 'a' on the third insert, the test re-inserted 'a' and then
asserted 'b' was STILL a duplicate. That is not reachable under a strict
capacity-2 FIFO: re-inserting 'a' is itself a fourth distinct entry, and it
evicts the then-oldest surviving key -- 'b', not 'a' again -- leaving only
'c' and the re-added 'a' in the window. The implementation's eviction is a
plain bounded FIFO exactly matching its own doc comment; the test's premise
that two keys survive a third eviction under capacity 2 was arithmetically
impossible, not a defect in DedupeFilter.

Corrected the assertion to check 'c' (the key that is actually still
resident) for the expected duplicate, and added a check that 'b' now reads
as new -- since it was evicted -- rather than silently leaving that half of
the scenario unverified. No production code changed; DedupeFilter's
eviction logic is untouched.
The nextest run finally reaching this suite (after this PR's other
compile-blocking fixes) surfaced another arithmetic mistake in a test's
literal expectation, not a defect in identifier().

identifier() keeps exactly the last 4 raw characters of its input and
prefixes them with '...' -- verified elsewhere in this same file by
keeps_only_last_four_chars("12345") => "...2345". For a Matrix id like
"@alice:example.org", the last 4 characters are '.', 'o', 'r', 'g' (the
domain's leading dot falls inside that window), so the correct output is
"....org" (four dots). The test asserted "...org" (three dots), which
silently assumed "org" alone occupied the 4-character window and forgot
the preceding dot -- an assumption the function was never written to honor
and that no other test in this file supports.

Corrected both assertions to the byte-for-byte actual output and documented
why the count is four, not three, so the next reader does not repeat the
same miscount. No production code changed; identifier()'s last-4-characters
contract is untouched and remains uniform across phone numbers and Matrix
ids.
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant