Files
buzz/desktop/src/shared/deep-link.test.mjs
Taylor Ho 5acb930821 feat(desktop-messages): render compact Buzz permalink chips (#5638)
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now open reliably and display recognizable context in the
desktop app.
**Problem:** Buzz links could appear as raw or ambiguous URLs, and
navigation links received during startup or community transitions could
be dropped before the UI was ready. Repository and issue shares in
particular required hover context to understand at a glance.
**Solution:** Queue desktop channel/message navigation until the UI is
ready, then render bare Buzz permalinks as icon-prefixed chips with
concise entity context while preserving user-authored Markdown labels as
ordinary links.

<details>
<summary>File changes</summary>

**desktop/src-tauri/src/deep_link.rs**
Adds validated channel-link parsing and a deduplicated, acknowledged
queue so navigation survives frontend startup.

**desktop/src-tauri/src/lib.rs**
Registers the pending-navigation state and commands with the desktop
application.

**desktop/src/features/communities/useCommunityInit.ts**
Resets queued navigation safely across community boundaries without
leaking stale destinations.

**desktop/src/features/messages/lib/channelLink.test.mjs**
Covers valid, malformed, and canonical channel permalink forms.

**desktop/src/features/messages/lib/channelLink.ts**
Defines strict parsing and detection for `buzz://channel/<uuid>` links.

**desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs**
Extends composer-node coverage for normalized Buzz link content.

**desktop/src/features/messages/lib/composerMessageLinkNode.ts**
Keeps composer link-node handling aligned with the expanded Buzz link
surface.

**desktop/src/features/messages/lib/remarkChannelDeepLinks.test.mjs**
Verifies bare channel URLs become renderable deep-link nodes without
touching code.

**desktop/src/features/messages/lib/remarkChannelDeepLinks.ts**
Transforms eligible bare channel links into dedicated Markdown nodes.

**desktop/src/features/messages/lib/remarkEntityLinks.test.mjs**
Covers bare repository, pull-request, and issue detection and code-span
exclusions.

**desktop/src/features/messages/lib/remarkEntityLinks.ts**
Adds dedicated Markdown nodes for bare Buzz project entities.

**desktop/src/shared/deep-link.test.mjs**
Exercises queued navigation, acknowledgement, serialization, and
community-switch behavior.

**desktop/src/shared/deep-link.ts**
Serializes pending deep-link drains and acknowledges destinations only
after successful navigation.

**desktop/src/shared/styles/globals/markdown.css**
Aligns permalink icon geometry and spacing with agent mention chips.

**desktop/src/shared/ui/markdown.test.mjs**
Adds integration coverage for every permalink chip, authored labels,
fallbacks, icons, and static rendering.

**desktop/src/shared/ui/markdown.tsx**
Routes channel and entity nodes through the shared presentation path
while preserving authored link text.

**desktop/src/shared/ui/markdown/BuzzLinkChip.tsx**
Introduces the shared interactive/static permalink chip and
authored-label inline-link components.

**desktop/src/shared/ui/markdown/ChannelDeepLink.tsx**
Renders channel shares and references with Hash icons, names, and
shortened-ID fallbacks.

**desktop/src/shared/ui/markdown/MessageLinkPill.tsx**
Renders ordinary message shares with message icons and channel/message
context while retaining sent-from-thread behavior.

**desktop/src/shared/ui/markdown/entityLinks.tsx**
Maps repositories, pull requests, and issues to Projects-aligned icons
and contextual labels.

**desktop/src/shared/ui/markdown/nodeCache.ts**
Includes entity-link rendering in cached Markdown node handling.

**desktop/src/shared/ui/markdown/utils.ts**
Allows validated channel links through the Buzz URL transform.

**desktop/src/shared/useMessageDeepLinks.ts**
Drains queued navigation links safely and clears them during teardown.

**desktop/src/testing/e2eBridge.ts**
Extends the mock bridge with pending-navigation command behavior.

**desktop/tests/e2e/community-rail.spec.ts**
Verifies queued links do not cross community boundaries.

**desktop/tests/e2e/navigation.spec.ts**
Covers channel/message deep-link navigation during startup and active
sessions.

**desktop/tests/helpers/bridge.ts**
Adds reusable deep-link mock state and acknowledgement helpers.


</details>

## Reproduction steps
1. Run the desktop app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
in warm and cold-start states.

## Screenshots / demos
Houston dark theme with custom purple accent (`#a855f7`), captured from
rebased visual implementation `ad411cc06`; current head `0aafa144f` only
adjusts E2E expectations for the visible mention-label behavior shown
here.

**Composer — channel, message, repository, pull request, and issue
pills**

![Composer with all Buzz permalink pill types in Houston dark theme and
purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5638/composer-all-permalink-pills-dark-purple.png)

**Message list — channel, message, repository, pull request, and issue
pills**

![Message list with all Buzz permalink pill types in Houston dark theme
and purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5638/message-list-all-pill-types-dark-purple.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-14 10:18:18 -07:00

400 lines
10 KiB
JavaScript

import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
const ipcHandlers = new Map();
let nextCallbackId = 1;
const callbacks = new Map();
const tauriInternals = {
invoke: (cmd, args) => {
const handler = ipcHandlers.get(cmd);
if (handler) return Promise.resolve(handler(args));
return Promise.reject(new Error(`unmocked Tauri command: ${cmd}`));
},
transformCallback: (callback) => {
const id = nextCallbackId++;
callbacks.set(id, callback);
return id;
},
};
globalThis.window = {
__TAURI_INTERNALS__: tauriInternals,
__TAURI_EVENT_PLUGIN_INTERNALS__: { unregisterListener: () => {} },
};
globalThis.__TAURI_INTERNALS__ = tauriInternals;
const { listenForNavigationDeepLinks, resetNavigationDeepLinkDrain } =
await import("@/shared/deep-link.ts");
function deferred() {
let resolve;
const promise = new Promise((done) => {
resolve = done;
});
return { promise, resolve };
}
async function settle() {
await new Promise((resolve) => setTimeout(resolve, 0));
}
afterEach(() => {
ipcHandlers.clear();
callbacks.clear();
});
test("listener teardown leaves an unaccepted FIFO item for the next mount", async () => {
const queue = [
{
id: "first",
kind: "channel",
channelId: "channel-1",
messageId: null,
threadRootId: null,
},
{
id: "second",
kind: "message",
channelId: "channel-2",
messageId: "message-2",
threadRootId: "root-2",
},
];
const firstAcknowledge = deferred();
const acknowledged = [];
let unlistenCount = 0;
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {
unlistenCount += 1;
});
ipcHandlers.set("take_pending_navigation_deep_link", () => queue[0] ?? null);
ipcHandlers.set(
"acknowledge_pending_navigation_deep_link",
async ({ id }) => {
if (id === "first") await firstAcknowledge.promise;
assert.equal(queue[0]?.id, id);
acknowledged.push(id);
queue.shift();
return true;
},
);
let firstMountActive = true;
const firstOpened = [];
const firstUnlisten = await listenForNavigationDeepLinks(
(payload) => {
if (!firstMountActive) return false;
firstOpened.push(payload.channelId);
return true;
},
(payload) => {
if (!firstMountActive) return false;
firstOpened.push(payload.messageId);
return true;
},
);
await settle();
assert.deepEqual(firstOpened, ["channel-1"]);
firstMountActive = false;
firstUnlisten();
firstAcknowledge.resolve();
await settle();
assert.deepEqual(acknowledged, ["first"]);
assert.equal(queue[0]?.id, "second");
const secondOpened = [];
const secondUnlisten = await listenForNavigationDeepLinks(
(payload) => {
secondOpened.push(payload.channelId);
return true;
},
(payload) => {
secondOpened.push(payload.messageId);
return true;
},
);
await settle();
assert.deepEqual(secondOpened, ["message-2"]);
assert.deepEqual(acknowledged, ["first", "second"]);
assert.equal(queue.length, 0);
secondUnlisten();
assert.equal(unlistenCount, 4);
});
test("concurrent listener remount does not take or acknowledge the in-flight head twice", async () => {
const queue = [
{
id: "in-flight",
kind: "channel",
channelId: "channel-1",
messageId: null,
threadRootId: null,
},
];
const acknowledgeGate = deferred();
const opened = [];
const acknowledged = [];
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("take_pending_navigation_deep_link", () => queue[0] ?? null);
ipcHandlers.set(
"acknowledge_pending_navigation_deep_link",
async ({ id }) => {
acknowledged.push(id);
await acknowledgeGate.promise;
assert.equal(queue[0]?.id, id);
queue.shift();
return true;
},
);
const firstUnlisten = await listenForNavigationDeepLinks(
(payload) => {
opened.push(`first:${payload.channelId}`);
return true;
},
() => true,
);
await settle();
assert.deepEqual(opened, ["first:channel-1"]);
assert.deepEqual(acknowledged, ["in-flight"]);
firstUnlisten();
const secondUnlisten = await listenForNavigationDeepLinks(
(payload) => {
opened.push(`second:${payload.channelId}`);
return true;
},
() => true,
);
await settle();
assert.deepEqual(opened, ["first:channel-1"]);
assert.deepEqual(acknowledged, ["in-flight"]);
acknowledgeGate.resolve();
await settle();
await settle();
assert.deepEqual(opened, ["first:channel-1"]);
assert.deepEqual(acknowledged, ["in-flight"]);
assert.equal(queue.length, 0);
secondUnlisten();
});
test("community reset prevents an in-flight route from acknowledging", async () => {
const pending = {
id: "old-community",
kind: "channel",
channelId: "channel-1",
messageId: null,
threadRootId: null,
};
const routeGate = deferred();
let acknowledgeCount = 0;
let clearCount = 0;
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("clear_pending_navigation_deep_links", () => {
clearCount += 1;
});
ipcHandlers.set("take_pending_navigation_deep_link", () => pending);
ipcHandlers.set("acknowledge_pending_navigation_deep_link", () => {
acknowledgeCount += 1;
return true;
});
const unlisten = await listenForNavigationDeepLinks(
async () => {
await routeGate.promise;
return true;
},
() => true,
);
await settle();
await resetNavigationDeepLinkDrain();
routeGate.resolve();
await settle();
assert.equal(clearCount, 1);
assert.equal(acknowledgeCount, 0);
unlisten();
});
test("community reset after take does not route the stale item", async () => {
const takeGate = deferred();
const opened = [];
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("clear_pending_navigation_deep_links", () => {});
ipcHandlers.set("take_pending_navigation_deep_link", async () => {
await takeGate.promise;
return {
id: "old-community",
kind: "channel",
channelId: "channel-1",
messageId: null,
threadRootId: null,
};
});
ipcHandlers.set("acknowledge_pending_navigation_deep_link", () => true);
const unlisten = await listenForNavigationDeepLinks(
(payload) => {
opened.push(payload.channelId);
return true;
},
() => true,
);
await settle();
await resetNavigationDeepLinkDrain();
takeGate.resolve();
await settle();
assert.deepEqual(opened, []);
unlisten();
});
test("community reset stops the stale drain before taking another item", async () => {
const queue = [
{
id: "first",
kind: "channel",
channelId: "channel-1",
messageId: null,
threadRootId: null,
},
{
id: "second",
kind: "channel",
channelId: "channel-2",
messageId: null,
threadRootId: null,
},
];
const opened = [];
let takeCount = 0;
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("clear_pending_navigation_deep_links", () => {
queue.length = 0;
});
ipcHandlers.set("take_pending_navigation_deep_link", () => {
takeCount += 1;
return queue[0] ?? null;
});
ipcHandlers.set(
"acknowledge_pending_navigation_deep_link",
async ({ id }) => {
assert.equal(queue[0]?.id, id);
queue.shift();
await resetNavigationDeepLinkDrain();
return true;
},
);
const unlisten = await listenForNavigationDeepLinks(
(payload) => {
opened.push(payload.channelId);
return true;
},
() => true,
);
await settle();
await settle();
assert.deepEqual(opened, ["channel-1"]);
assert.equal(takeCount, 1);
unlisten();
});
test("failed community clear quarantines stale navigation from the next listener", async () => {
const stale = {
id: "old-community",
kind: "channel",
channelId: "old-channel",
messageId: null,
threadRootId: null,
};
const opened = [];
let takeCount = 0;
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("clear_pending_navigation_deep_links", () => {
throw new Error("clear failed");
});
ipcHandlers.set("take_pending_navigation_deep_link", () => {
takeCount += 1;
return stale;
});
await assert.rejects(resetNavigationDeepLinkDrain(), /clear failed/);
const unlisten = await listenForNavigationDeepLinks(
(payload) => {
opened.push(payload.channelId);
return true;
},
() => true,
);
await settle();
assert.equal(takeCount, 0);
assert.deepEqual(opened, []);
unlisten();
// Restore the module-level gate for later tests, just as a successful retry
// does in the application.
ipcHandlers.set("clear_pending_navigation_deep_links", () => {});
await resetNavigationDeepLinkDrain();
});
test("rejected navigation remains queued and is not acknowledged", async () => {
const pending = {
id: "retry-me",
kind: "channel",
channelId: "channel-1",
messageId: null,
threadRootId: null,
};
let acknowledgeCount = 0;
const warnings = [];
const originalWarn = console.warn;
ipcHandlers.set("plugin:event|listen", () => nextCallbackId);
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("take_pending_navigation_deep_link", () => pending);
ipcHandlers.set("acknowledge_pending_navigation_deep_link", () => {
acknowledgeCount += 1;
return true;
});
console.warn = (...args) => warnings.push(args);
try {
const unlisten = await listenForNavigationDeepLinks(
async () => {
throw new Error("route failed");
},
async () => true,
);
await settle();
assert.equal(acknowledgeCount, 0);
assert.equal(warnings.length, 1);
assert.match(String(warnings[0][1]), /route failed/);
unlisten();
} finally {
console.warn = originalWarn;
}
});