Skip to content

Commit 7207b3f

Browse files
committed
fix(mattermost): key public channels as channel in directory listing
listMattermostDirectoryGroups labeled every joined channel — public `O` and private `P` — as kind `group`. A name-resolved public channel could then be keyed as `mattermost:group:<id>` on outbound routing, forking a phantom group session and splitting the transcript from the inbound `channel:<id>` one. Derive the kind from the authoritative Mattermost channel type (`O` -> channel, `P`/`G` -> group) and add a regression test. This closes the public-channel regression path flagged in review for #95646 while keeping private channels keyed as `group`.
1 parent ff7c958 commit 7207b3f

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

extensions/mattermost/src/mattermost/directory.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,35 @@ describe("mattermost directory", () => {
8585
).resolves.toEqual([{ kind: "group", id: "channel:chan-2", name: "ops", handle: "Ops" }]);
8686
});
8787

88+
it("labels public O channels as channel and private P channels as group (#95646)", async () => {
89+
// A public `O` channel must NOT be keyed as `group`, otherwise a name-resolved
90+
// public channel forks a phantom `group:<id>` session on outbound routing.
91+
const client = {
92+
token: "token-default",
93+
request: vi.fn().mockResolvedValueOnce([
94+
{ id: "pub-1", type: "O", name: "general", display_name: "General" },
95+
{ id: "priv-1", type: "P", name: "secret", display_name: "Secret" },
96+
{ id: "dm-1", type: "D", name: "dm", display_name: "DM" },
97+
]),
98+
};
99+
100+
listMattermostAccountIdsMock.mockReturnValue(["default"]);
101+
resolveMattermostAccountMock.mockReturnValue({
102+
enabled: true,
103+
botToken: "token-default",
104+
baseUrl: "https://chat.example.com",
105+
});
106+
createMattermostClientMock.mockReturnValueOnce(client);
107+
fetchMattermostMeMock.mockResolvedValue({ id: "me-1" });
108+
109+
await expect(
110+
listMattermostDirectoryGroups({ cfg: {} as never, runtime: {} as never }),
111+
).resolves.toEqual([
112+
{ kind: "channel", id: "channel:pub-1", name: "general", handle: "General" },
113+
{ kind: "group", id: "channel:priv-1", name: "secret", handle: "Secret" },
114+
]);
115+
});
116+
88117
it("uses the first healthy client for peers and filters self and blanks", async () => {
89118
const client = {
90119
token: "token-default",

extensions/mattermost/src/mattermost/directory.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type MattermostClient,
1010
type MattermostUser,
1111
} from "./client.js";
12+
import { mapMattermostChannelTypeToChatType } from "./monitor-gating.js";
1213
import type { ChannelDirectoryEntry, OpenClawConfig, RuntimeEnv } from "./runtime-api.js";
1314

1415
export type MattermostDirectoryParams = {
@@ -97,7 +98,15 @@ export async function listMattermostDirectoryGroups(
9798
}
9899
seenIds.add(ch.id);
99100
entries.push({
100-
kind: "group" as const,
101+
// Authoritative per-channel kind: a public `O` channel is a `channel`;
102+
// only private `P` / group `G` map to `group`. Emitting a blanket
103+
// `group` here mislabels public channels, so a name-resolved public
104+
// channel would fork a phantom `group:<id>` session on outbound
105+
// routing (#95646).
106+
kind:
107+
mapMattermostChannelTypeToChatType(ch.type) === "group"
108+
? ("group" as const)
109+
: ("channel" as const),
101110
id: `channel:${ch.id}`,
102111
name: ch.name ?? undefined,
103112
handle: ch.display_name ?? undefined,

0 commit comments

Comments
 (0)