9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
204 lines
8.8 KiB
TypeScript
204 lines
8.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge, openNewMessagePage } from "../helpers/bridge";
|
|
|
|
// Guards the NIP-IA discovery-suppression contract (Dawn's table v2):
|
|
// - Members sidebar: archived members fold under "Archived (N)", not in the
|
|
// active people/bots lists.
|
|
// - Mention autocomplete: archived members are filtered out of suggestions
|
|
// (but their `members` entry still resolves historical @-mentions).
|
|
// - DM picker: archived users are omitted from search results.
|
|
// - History invariant: an archived user's existing channel message renders
|
|
// normally (Tyler's "never hide messages").
|
|
|
|
const ALICE_PUBKEY =
|
|
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";
|
|
const BOB_PUBKEY =
|
|
"bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260";
|
|
// Active mock identity (matches DEFAULT_MOCK_IDENTITY.pubkey in e2eBridge).
|
|
// Used to exercise the self-exemption rule in the predicate.
|
|
const SELF_PUBKEY = "deadbeef".repeat(8);
|
|
|
|
test.describe("NIP-IA hide archived from discovery", () => {
|
|
test("members sidebar: archived member folds under Archived section", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { archivedIdentities: [ALICE_PUBKEY] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
|
|
// Active People list should NOT include Alice.
|
|
const peopleList = page.getByTestId("members-sidebar-people");
|
|
await expect(peopleList).not.toContainText("alice");
|
|
|
|
// Folded Archived section is visible with count = 1.
|
|
await expect(page.getByTestId("members-sidebar-archived")).toBeVisible();
|
|
await expect(page.getByTestId("members-sidebar-archived-count")).toHaveText(
|
|
"(1)",
|
|
);
|
|
|
|
// Expanding it reveals Alice.
|
|
await page.getByTestId("members-sidebar-archived").click();
|
|
await expect(
|
|
page.getByTestId("members-sidebar-archived-list"),
|
|
).toContainText("alice");
|
|
});
|
|
|
|
test("members sidebar: no Archived section when no archived members", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { archivedIdentities: [] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
await expect(page.getByTestId("members-sidebar-archived")).toHaveCount(0);
|
|
});
|
|
|
|
test("mention autocomplete: archived member is filtered out", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { archivedIdentities: [ALICE_PUBKEY] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
// Type `@a` to trigger autocomplete. Without filtering Alice would match.
|
|
const input = page.getByTestId("message-input");
|
|
await input.click();
|
|
await input.pressSequentially("@a");
|
|
|
|
// Alice's suggestion testid does NOT appear.
|
|
await expect(
|
|
page.getByTestId(`mention-suggestion-${ALICE_PUBKEY}`),
|
|
).toHaveCount(0);
|
|
|
|
// Sanity: Bob (also `@b...` candidate) is available when his query starts;
|
|
// proves the autocomplete itself is functional, not just always-empty.
|
|
await input.fill("@b");
|
|
await expect(
|
|
page.getByTestId(`mention-suggestion-${BOB_PUBKEY}`),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("DM picker: archived user is omitted from search results", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { archivedIdentities: [ALICE_PUBKEY] });
|
|
await page.goto("/");
|
|
await openNewMessagePage(page);
|
|
await expect(page.getByTestId("new-message-page")).toBeVisible();
|
|
|
|
await page.getByTestId("new-dm-search").fill("alice");
|
|
// Alice's result row does NOT appear, even though search would normally
|
|
// surface her by display name.
|
|
await expect(page.getByTestId(`new-dm-result-${ALICE_PUBKEY}`)).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
// Sanity: Bob is still searchable, confirming the dialog works.
|
|
await page.getByTestId("new-dm-search").fill("bob");
|
|
await expect(page.getByTestId(`new-dm-result-${BOB_PUBKEY}`)).toBeVisible();
|
|
});
|
|
|
|
test("history invariant: archived user's existing message still renders with their name", async ({
|
|
page,
|
|
}) => {
|
|
// Alice has a seeded message in #general from the prior PR's e2e setup
|
|
// (see e2eBridge.ts seed). Archiving her must not remove that message.
|
|
await installMockBridge(page, { archivedIdentities: [ALICE_PUBKEY] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
// Alice's seed message renders with her display name in the timeline.
|
|
const aliceMessage = page.getByTestId("message-row").nth(1);
|
|
await expect(aliceMessage).toContainText("alice");
|
|
await expect(aliceMessage).toContainText("Hey team — checking in.");
|
|
});
|
|
|
|
test("self-exemption: archived current user still appears in their own People list (not folded)", async ({
|
|
page,
|
|
}) => {
|
|
// Anti-shadowban property: the current user is never hidden from their
|
|
// own client even when archived on the relay. The predicate's
|
|
// self-exemption is what enforces this — without it, the user would lose
|
|
// their own seat in the members sidebar.
|
|
await installMockBridge(page, { archivedIdentities: [SELF_PUBKEY] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
|
|
// Self appears in active People list — NOT folded into Archived.
|
|
const peopleList = page.getByTestId("members-sidebar-people");
|
|
await expect(peopleList).toContainText("You");
|
|
await expect(
|
|
peopleList.getByTestId(`sidebar-member-${SELF_PUBKEY}`),
|
|
).toBeVisible();
|
|
// No Archived section at all when self is the only archived pubkey in
|
|
// the channel (self-exemption makes archived.length === 0).
|
|
await expect(page.getByTestId("members-sidebar-archived")).toHaveCount(0);
|
|
});
|
|
|
|
test("self-exemption: archived current user can still self-mention in own autocomplete", async ({
|
|
page,
|
|
}) => {
|
|
// Self-mention is a no-op in practice, but the predicate must not filter
|
|
// self from their own autocomplete — that would be the shadowban NIP-IA
|
|
// exists to prevent.
|
|
await installMockBridge(page, { archivedIdentities: [SELF_PUBKEY] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
const input = page.getByTestId("message-input");
|
|
await input.click();
|
|
await input.pressSequentially("@npub");
|
|
// Self's suggestion appears in autocomplete despite being in the archived
|
|
// set — the predicate's self-exemption fires.
|
|
await expect(
|
|
page.getByTestId(`mention-suggestion-${SELF_PUBKEY}`),
|
|
).toBeVisible();
|
|
});
|
|
|
|
// Member-adder (ChannelMemberInviteCard) — the invite search excludes
|
|
// existing channel members, so we test in #agents where Bob is NOT a
|
|
// member (only the mock identity + Charlie are): the ONLY thing that can
|
|
// drop Bob from results is the archive filter. The control run (nothing
|
|
// archived) proves he would otherwise appear — guarding a vacuous green.
|
|
test("member-adder: archived non-member is omitted from invite search", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { archivedIdentities: [BOB_PUBKEY] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-agents").click();
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
await page.getByTestId("channel-management-search-users").fill("bob");
|
|
await expect(
|
|
page.getByTestId(`channel-user-search-result-${BOB_PUBKEY}`),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("member-adder: control — non-archived non-member IS in invite search", async ({
|
|
page,
|
|
}) => {
|
|
// Same channel/query, nothing archived: Bob now appears. This is the
|
|
// companion that makes the test above non-vacuous (proves he was dropped
|
|
// by the archive filter, not by member-exclusion or a broken search).
|
|
await installMockBridge(page, { archivedIdentities: [] });
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-agents").click();
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
await page.getByTestId("channel-management-search-users").fill("bob");
|
|
await expect(
|
|
page.getByTestId(`channel-user-search-result-${BOB_PUBKEY}`),
|
|
).toBeVisible();
|
|
});
|
|
});
|