mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
fix(cli): honor Teams message search targets and limits (#149353)
## What Problem This Solves Teams CLI search required Discord's guild flag, then silently returned 25 matches when 30 were requested. Fixes #149352. ## Why This Change Was Made Each channel validates its identifiers. Teams now uses the existing positive-integer reader. The search result state has one writer: the existing Graph loop. Production delta: registration +3 lines; Teams adapter +1 line. Growth accepted under the owner's standing acceptance (2026-09-15). ## User Impact Teams search works without a guild and honors the requested bounded limit. ## Evidence `openclaw message search --channel msteams --channel-id <team-id>/<channel-id> --query planning --limit 30 --json` Real command registration → messageCommand → Teams → loopback HTTP: the base rejects a missing guild. With registration alone fixed, it returns 25 matches with truncated=true. Both fixes return 30 matches with truncated=false. Requesting 60 returns 50 with truncated=true. The adapter rejects malformed limits before token acquisition or HTTP; separate CLI helper tests cover the full command wrapper's earlier flag validation. Live tenant authentication was not tested. ## Compatibility Discord still requires a resolved guild and retains its exact adapter error when resolution fails. The CLI can now use the existing channel-to-guild resolution. Delegated Teams access refusals pass on base and candidate. Direct operators retain their existing explicit-conversation access policy. No configuration or schema change. ## Consumers CLI search, Teams adapter, and CLI documentation. ## Invalidation No cache or persistent state changes. ## Tests 457 tests passed on the merged candidate: Teams CLI/Graph/actions, Discord actions, and CLI helpers/registration. Base authorization and Discord controls: 265 passed. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
e9a73c210d
commit
2a7f2d126f
4 changed files with 70 additions and 34 deletions
|
|
@ -113,7 +113,7 @@ unresolved SecretRef on the selected channel/account fails the action closed.
|
|||
| `pin` / `unpin` | Discord, Matrix, Microsoft Teams, Slack | `--message-id`, `--target` | `unpin` also accepts `--pinned-message-id` (Microsoft Teams: the pin/list-pins resource id, not the chat message id). |
|
||||
| `pins` (list) | Discord, Matrix, Microsoft Teams, Slack | `--target` | `--limit`. |
|
||||
| `permissions` | Discord, Matrix | `--target` | Matrix: available only when encryption is enabled and verification actions are allowed. |
|
||||
| `search` | Discord | `--guild-id`, `--query` | `--channel-id`, `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit`. |
|
||||
| `search` | Discord, Microsoft Teams | `--query` | `--guild-id` (Discord; resolved from `--channel-id` when omitted), `--channel-id` (required for Microsoft Teams as Graph `<team-id>/<channel-id>`), `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit`. |
|
||||
| `member info` | Discord, Matrix, Microsoft Teams, Slack | `--user-id` | `--channel-id` (required for Matrix and Microsoft Teams), `--guild-id` (Discord). |
|
||||
|
||||
Reaction listings show labels, counts, and available users as plain terminal text.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Msteams plugin module implements channel behavior.
|
||||
import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
|
||||
import { readPositiveIntegerParam } from "openclaw/plugin-sdk/channel-actions";
|
||||
import type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageToolDiscovery,
|
||||
|
|
@ -891,7 +892,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
|
|||
if (!query) {
|
||||
return actionError("Search requires a target (to) and query.");
|
||||
}
|
||||
const limit = typeof ctx.params.limit === "number" ? ctx.params.limit : undefined;
|
||||
const limit = readPositiveIntegerParam(ctx.params, "limit");
|
||||
const from =
|
||||
typeof ctx.params.from === "string" ? ctx.params.from.trim() : undefined;
|
||||
const { searchMessagesMSTeams } = await loadMSTeamsChannelRuntime();
|
||||
|
|
|
|||
|
|
@ -14,13 +14,16 @@ export function registerMessagePermissionsCommand(message: Command, helpers: Mes
|
|||
.action((opts) => helpers.runMessageAction("permissions", opts));
|
||||
}
|
||||
|
||||
/** Register Discord message search command and repeatable filters. */
|
||||
/** Register the channel message search command and repeatable filters. */
|
||||
export function registerMessageSearchCommand(message: Command, helpers: MessageCliHelpers) {
|
||||
helpers
|
||||
.withMessageBase(message.command("search").description("Search Discord messages"))
|
||||
.requiredOption("--guild-id <id>", "Guild id")
|
||||
.withMessageBase(message.command("search").description("Search messages"))
|
||||
.requiredOption("--query <text>", "Search query")
|
||||
.option("--channel-id <id>", "Channel id")
|
||||
.option("--guild-id <id>", "Guild id (Discord)")
|
||||
.option(
|
||||
"--channel-id <id>",
|
||||
"Channel id (Discord) or Graph team-id/channel-id (Microsoft Teams)",
|
||||
)
|
||||
.option("--channel-ids <id>", "Channel id (repeat)", collectOption, [] as string[])
|
||||
.option("--author-id <id>", "Author id")
|
||||
.option("--author-ids <id>", "Author id (repeat)", collectOption, [] as string[])
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { ChannelThreadingToolContext } from "../src/channels/plugins/types.
|
|||
import { createDefaultDeps } from "../src/cli/deps.js";
|
||||
import { createMessageCliHelpers } from "../src/cli/program/message/helpers.js";
|
||||
import { registerMessageDiscordAdminCommands } from "../src/cli/program/message/register.discord-admin.js";
|
||||
import { registerMessageSearchCommand } from "../src/cli/program/message/register.permissions-search.js";
|
||||
import { messageCommand } from "../src/commands/message.js";
|
||||
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../src/config/config.js";
|
||||
import type { OpenClawConfig } from "../src/config/types.js";
|
||||
|
|
@ -118,6 +119,7 @@ afterEach(async () => {
|
|||
async function createFixture(
|
||||
currentContext: "channel" | "chat" | "none" = "channel",
|
||||
origin: "bundled" | "global" = "bundled",
|
||||
searchMatches = 1,
|
||||
) {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
|
|
@ -236,12 +238,10 @@ async function createFixture(
|
|||
path === `/v1.0/teams/${destination.teamId}/channels/${destination.channelId}/messages`
|
||||
) {
|
||||
body = {
|
||||
value: [
|
||||
{
|
||||
id: destination.messageId,
|
||||
body: { content: destination.text, contentType: "text" },
|
||||
},
|
||||
],
|
||||
value: Array.from({ length: searchMatches }, (_, index) => ({
|
||||
id: index === 0 ? destination.messageId : `${destination.messageId}-${index}`,
|
||||
body: { content: destination.text, contentType: "text" },
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
response.statusCode = 404;
|
||||
|
|
@ -399,45 +399,77 @@ function expectGraphRequests(requests: GraphRequest[], action: Action, destinati
|
|||
);
|
||||
}
|
||||
|
||||
describe("Teams member info CLI", () => {
|
||||
it("reads a selected channel member without current conversation context", async () => {
|
||||
const fixture = await createFixture("none");
|
||||
describe("Teams message CLI", () => {
|
||||
async function runCli(
|
||||
register: typeof registerMessageDiscordAdminCommands,
|
||||
args: string[],
|
||||
action: Action,
|
||||
) {
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const command = new Command().name("message").exitOverride();
|
||||
registerMessageDiscordAdminCommands(command, {
|
||||
register(command, {
|
||||
...createMessageCliHelpers("msteams"),
|
||||
runMessageAction: async (action, opts) => {
|
||||
await messageCommand({ ...opts, action }, createDefaultDeps(), runtime);
|
||||
runMessageAction: async (name, opts) => {
|
||||
await messageCommand({ ...opts, action: name }, createDefaultDeps(), runtime);
|
||||
},
|
||||
});
|
||||
|
||||
await command.parseAsync(
|
||||
[
|
||||
"member",
|
||||
"info",
|
||||
"--channel",
|
||||
"msteams",
|
||||
"--user-id",
|
||||
memberId,
|
||||
"--channel-id",
|
||||
otherTarget,
|
||||
"--json",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
await command.parseAsync([...args, "--channel", "msteams", "--json"], { from: "user" });
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
const result = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
expect(result).toMatchObject({
|
||||
action: "member-info",
|
||||
action,
|
||||
channel: "msteams",
|
||||
dryRun: false,
|
||||
handledBy: "plugin",
|
||||
});
|
||||
expectReadResult(result.payload, "member-info", other);
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
it("reads a selected channel member without current conversation context", async () => {
|
||||
const fixture = await createFixture("none");
|
||||
const payload = await runCli(
|
||||
registerMessageDiscordAdminCommands,
|
||||
["member", "info", "--user-id", memberId, "--channel-id", otherTarget],
|
||||
"member-info",
|
||||
);
|
||||
expectReadResult(payload, "member-info", other);
|
||||
expectGraphRequests(fixture.requests, "member-info", other);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ limit: 30, count: 30, truncated: false },
|
||||
{ limit: 60, count: 50, truncated: true },
|
||||
])(
|
||||
"searches a selected channel with limit $limit and no guild",
|
||||
async ({ limit, count, truncated }) => {
|
||||
const fixture = await createFixture("none", "bundled", limit);
|
||||
const payload = await runCli(
|
||||
registerMessageSearchCommand,
|
||||
["search", "--channel-id", otherTarget, "--query", "planning", "--limit", String(limit)],
|
||||
"search",
|
||||
);
|
||||
expect(payload).toMatchObject({ ok: true, channel: "msteams", action: "search" });
|
||||
expect(payload.messages).toHaveLength(count);
|
||||
expect(payload.truncated).toBe(truncated);
|
||||
expectGraphRequests(fixture.requests, "search", other);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a malformed limit in the registered CLI search adapter", async () => {
|
||||
const fixture = await createFixture("none");
|
||||
await expect(
|
||||
runCli(
|
||||
registerMessageSearchCommand,
|
||||
["search", "--channel-id", otherTarget, "--query", "planning", "--limit", "abc"],
|
||||
"search",
|
||||
),
|
||||
).rejects.toThrow("limit must be a positive integer");
|
||||
expect(fixture.requests).toEqual([]);
|
||||
expect(graph.acquireToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["tool", "gateway"] as const)("Teams %s read target selection", (route) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue