Files
claudemesh/apps/cli/tests/unit/resolve-target.test.ts
Alejandro Gutiérrez 9075ae05df fix(cli): messages stop landing on the wrong sessions (wrong-peer noise, bug9)
A reply routinely fanned out to every session of the sender's member,
which is the "peers spread messages to the wrong peers" noise. Three
links fed each other:

- channel meta put the MEMBER key in from_pubkey/from_id, and the skill
  told the model to reply by from_name (repeatable across sessions).
  from_pubkey/from_id are now the sender's session key; the member key
  moves to from_member_pubkey; the skill says reply to from_pubkey.
- a DM to a member key was claimed by that member's daemon socket and
  shown to all its sessions. New daemon/resolve-target.ts is the single
  resolver for /v1/send: a member key maps to its only live session, is
  refused (409 member_target_ambiguous + candidates) when it has several,
  and needs --fanout to reach all of them on purpose. A duplicated
  display name or prefix is ambiguous instead of first-match; control-
  plane rows are never addressable.
- a session MCP whose token didn't resolve fell into the unfiltered SSE
  stream and rendered every sibling's DMs. The MCP now subscribes with
  scope=session and gets no message events without a resolved session.

Also: --self lists only the current mesh and requires a registered
session (it could message its own new row); a DM to a control-plane row
says so instead of the wrong "session pubkeys are ephemeral" hint, and
`peer list --all` tags those rows (bug9).

Spec: .artifacts/specs/2026-09-26-session-identity-and-routing.md §2-3, §6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:29:03 +01:00

68 lines
2.7 KiB
TypeScript

/**
* Spec 2026-09-26 §3: the "messages land on the wrong peers" noise.
* A member-key DM reached every session of that member; a duplicated
* display name silently picked the first row; control-plane rows were
* addressable. One resolver now decides for every /v1/send caller.
*/
import { describe, expect, it } from "vitest";
import { resolveDirectTarget, type RosterPeer } from "~/daemon/resolve-target.js";
const M1 = "a".repeat(64);
const M2 = "b".repeat(64);
const S1 = "1".repeat(64);
const S2 = "2".repeat(64);
const S3 = "3".repeat(64);
const roster: RosterPeer[] = [
{ pubkey: M1, memberPubkey: M1, displayName: "daemon", role: "control-plane" },
{ pubkey: S1, memberPubkey: M1, displayName: "Intra-Back" },
{ pubkey: S2, memberPubkey: M1, displayName: "Intra-Back" },
{ pubkey: S3, memberPubkey: M2, displayName: "portal-kit" },
];
describe("resolveDirectTarget", () => {
it("a session pubkey resolves to itself", () => {
expect(resolveDirectTarget(S1, roster)).toMatchObject({ ok: true, pubkey: S1 });
});
it("a member key with one live session resolves to that session", () => {
expect(resolveDirectTarget(M2, roster)).toMatchObject({ ok: true, pubkey: S3, note: "member_to_session" });
});
it("a member key with several sessions is refused, listing them", () => {
const r = resolveDirectTarget(M1, roster);
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.code).toBe("member_target_ambiguous");
expect(r.candidates.map((c) => c.pubkey).sort()).toEqual([S1, S2]);
}
});
it("--fanout keeps the member key on purpose", () => {
expect(resolveDirectTarget(M1, roster, { fanout: true })).toMatchObject({ ok: true, pubkey: M1, note: "member_fanout" });
});
it("an unknown full key stays (offline store-and-forward)", () => {
const k = "f".repeat(64);
expect(resolveDirectTarget(k, roster)).toMatchObject({ ok: true, pubkey: k });
});
it("a duplicated display name is ambiguous, not first-match", () => {
expect(resolveDirectTarget("intra-back", roster)).toMatchObject({ ok: false, code: "ambiguous" });
expect(resolveDirectTarget("portal-kit", roster)).toMatchObject({ ok: true, pubkey: S3 });
});
it("control-plane rows are never addressable by name", () => {
expect(resolveDirectTarget("daemon", roster)).toMatchObject({ ok: false, code: "not_found" });
});
it("a prefix prefers session keys and must be unique", () => {
expect(resolveDirectTarget("3333", roster)).toMatchObject({ ok: true, pubkey: S3 });
// member-key prefix of a member with two sessions → ambiguous
expect(resolveDirectTarget("aaaa", roster)).toMatchObject({ ok: false, code: "ambiguous" });
expect(resolveDirectTarget("bbbb", roster)).toMatchObject({ ok: true, pubkey: S3 });
});
});