@openclaw/msteams 2026.3.8-beta.1 → 2026.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.11
4
+
5
+ ### Changes
6
+ - Version alignment with core OpenClaw release numbers.
7
+
8
+ ## 2026.3.10
9
+
10
+ ### Changes
11
+
12
+ - Version alignment with core OpenClaw release numbers.
13
+
14
+ ## 2026.3.9
15
+
16
+ ### Changes
17
+
18
+ - Version alignment with core OpenClaw release numbers.
19
+
3
20
  ## 2026.3.8-beta.1
4
21
 
5
22
  ### Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/msteams",
3
- "version": "2026.3.8-beta.1",
3
+ "version": "2026.3.11",
4
4
  "description": "OpenClaw Microsoft Teams channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -54,10 +54,12 @@ describe("resolveMSTeamsUserAllowlist", () => {
54
54
 
55
55
  describe("resolveMSTeamsChannelAllowlist", () => {
56
56
  it("resolves team/channel by team name + channel display name", async () => {
57
- listTeamsByName.mockResolvedValueOnce([{ id: "team-1", displayName: "Product Team" }]);
57
+ // After the fix, listChannelsForTeam is called once and reused for both
58
+ // General channel resolution and channel matching.
59
+ listTeamsByName.mockResolvedValueOnce([{ id: "team-guid-1", displayName: "Product Team" }]);
58
60
  listChannelsForTeam.mockResolvedValueOnce([
59
- { id: "channel-1", displayName: "General" },
60
- { id: "channel-2", displayName: "Roadmap" },
61
+ { id: "19:general-conv-id@thread.tacv2", displayName: "General" },
62
+ { id: "19:roadmap-conv-id@thread.tacv2", displayName: "Roadmap" },
61
63
  ]);
62
64
 
63
65
  const [result] = await resolveMSTeamsChannelAllowlist({
@@ -65,14 +67,80 @@ describe("resolveMSTeamsChannelAllowlist", () => {
65
67
  entries: ["Product Team/Roadmap"],
66
68
  });
67
69
 
70
+ // teamId is now the General channel's conversation ID — not the Graph GUID —
71
+ // because that's what Bot Framework sends as channelData.team.id at runtime.
68
72
  expect(result).toEqual({
69
73
  input: "Product Team/Roadmap",
70
74
  resolved: true,
71
- teamId: "team-1",
75
+ teamId: "19:general-conv-id@thread.tacv2",
72
76
  teamName: "Product Team",
73
- channelId: "channel-2",
77
+ channelId: "19:roadmap-conv-id@thread.tacv2",
74
78
  channelName: "Roadmap",
75
79
  note: "multiple channels; chose first",
76
80
  });
77
81
  });
82
+
83
+ it("uses General channel conversation ID as team key for team-only entry", async () => {
84
+ // When no channel is specified we still resolve the General channel so the
85
+ // stored key matches what Bot Framework sends as channelData.team.id.
86
+ listTeamsByName.mockResolvedValueOnce([{ id: "guid-engineering", displayName: "Engineering" }]);
87
+ listChannelsForTeam.mockResolvedValueOnce([
88
+ { id: "19:eng-general@thread.tacv2", displayName: "General" },
89
+ { id: "19:eng-standups@thread.tacv2", displayName: "Standups" },
90
+ ]);
91
+
92
+ const [result] = await resolveMSTeamsChannelAllowlist({
93
+ cfg: {},
94
+ entries: ["Engineering"],
95
+ });
96
+
97
+ expect(result).toEqual({
98
+ input: "Engineering",
99
+ resolved: true,
100
+ teamId: "19:eng-general@thread.tacv2",
101
+ teamName: "Engineering",
102
+ });
103
+ });
104
+
105
+ it("falls back to Graph GUID when listChannelsForTeam throws", async () => {
106
+ // Edge case: API call fails (rate limit, network error). We fall back to
107
+ // the Graph GUID as the team key — the pre-fix behavior — so resolution
108
+ // still succeeds instead of propagating the error.
109
+ listTeamsByName.mockResolvedValueOnce([{ id: "guid-flaky", displayName: "Flaky Team" }]);
110
+ listChannelsForTeam.mockRejectedValueOnce(new Error("429 Too Many Requests"));
111
+
112
+ const [result] = await resolveMSTeamsChannelAllowlist({
113
+ cfg: {},
114
+ entries: ["Flaky Team"],
115
+ });
116
+
117
+ expect(result).toEqual({
118
+ input: "Flaky Team",
119
+ resolved: true,
120
+ teamId: "guid-flaky",
121
+ teamName: "Flaky Team",
122
+ });
123
+ });
124
+
125
+ it("falls back to Graph GUID when General channel is not found", async () => {
126
+ // Edge case: General channel was renamed or deleted. We fall back to the
127
+ // Graph GUID so resolution still succeeds rather than silently breaking.
128
+ listTeamsByName.mockResolvedValueOnce([{ id: "guid-ops", displayName: "Operations" }]);
129
+ listChannelsForTeam.mockResolvedValueOnce([
130
+ { id: "19:ops-announce@thread.tacv2", displayName: "Announcements" },
131
+ { id: "19:ops-random@thread.tacv2", displayName: "Random" },
132
+ ]);
133
+
134
+ const [result] = await resolveMSTeamsChannelAllowlist({
135
+ cfg: {},
136
+ entries: ["Operations"],
137
+ });
138
+
139
+ expect(result).toEqual({
140
+ input: "Operations",
141
+ resolved: true,
142
+ teamId: "guid-ops",
143
+ teamName: "Operations",
144
+ });
145
+ });
78
146
  });
@@ -120,11 +120,26 @@ export async function resolveMSTeamsChannelAllowlist(params: {
120
120
  return { input, resolved: false, note: "team not found" };
121
121
  }
122
122
  const teamMatch = teams[0];
123
- const teamId = teamMatch.id?.trim();
123
+ const graphTeamId = teamMatch.id?.trim();
124
124
  const teamName = teamMatch.displayName?.trim() || team;
125
- if (!teamId) {
125
+ if (!graphTeamId) {
126
126
  return { input, resolved: false, note: "team id missing" };
127
127
  }
128
+ // Bot Framework sends the General channel's conversation ID as
129
+ // channelData.team.id at runtime, NOT the Graph API group GUID.
130
+ // Fetch channels upfront so we can resolve the correct key format for
131
+ // runtime matching and reuse the list for channel lookups.
132
+ let teamChannels: Awaited<ReturnType<typeof listChannelsForTeam>> = [];
133
+ try {
134
+ teamChannels = await listChannelsForTeam(token, graphTeamId);
135
+ } catch {
136
+ // API failure (rate limit, network error) — fall back to Graph GUID as team key
137
+ }
138
+ const generalChannel = teamChannels.find((ch) => ch.displayName?.toLowerCase() === "general");
139
+ // Use the General channel's conversation ID as the team key — this
140
+ // matches what Bot Framework sends at runtime. Fall back to the Graph
141
+ // GUID if the General channel isn't found (renamed or deleted).
142
+ const teamId = generalChannel?.id?.trim() || graphTeamId;
128
143
  if (!channel) {
129
144
  return {
130
145
  input,
@@ -134,11 +149,11 @@ export async function resolveMSTeamsChannelAllowlist(params: {
134
149
  note: teams.length > 1 ? "multiple teams; chose first" : undefined,
135
150
  };
136
151
  }
137
- const channels = await listChannelsForTeam(token, teamId);
152
+ // Reuse teamChannels already fetched above
138
153
  const channelMatch =
139
- channels.find((item) => item.id === channel) ??
140
- channels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
141
- channels.find((item) =>
154
+ teamChannels.find((item) => item.id === channel) ??
155
+ teamChannels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
156
+ teamChannels.find((item) =>
142
157
  item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""),
143
158
  );
144
159
  if (!channelMatch?.id) {
@@ -151,7 +166,7 @@ export async function resolveMSTeamsChannelAllowlist(params: {
151
166
  teamName,
152
167
  channelId: channelMatch.id,
153
168
  channelName: channelMatch.displayName ?? channel,
154
- note: channels.length > 1 ? "multiple channels; chose first" : undefined,
169
+ note: teamChannels.length > 1 ? "multiple channels; chose first" : undefined,
155
170
  };
156
171
  },
157
172
  });