Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Rodrigo Uroz <rodrigouroz@users.noreply.github.com>
This commit is contained in:
@@ -37,6 +37,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
|
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
|
||||||
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
|
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
|
||||||
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
||||||
|
- Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.
|
||||||
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
||||||
- Providers: improve GitHub Copilot integration (enterprise support, base URL, and auth flow alignment).
|
- Providers: improve GitHub Copilot integration (enterprise support, base URL, and auth flow alignment).
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,20 @@ describe("handleSlackAction", () => {
|
|||||||
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
|
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes threadId through to readSlackMessages", async () => {
|
||||||
|
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||||
|
readSlackMessages.mockClear();
|
||||||
|
readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false });
|
||||||
|
|
||||||
|
await handleSlackAction(
|
||||||
|
{ action: "readMessages", channelId: "C1", threadId: "12345.6789" },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, opts] = readSlackMessages.mock.calls[0] ?? [];
|
||||||
|
expect(opts?.threadId).toBe("12345.6789");
|
||||||
|
});
|
||||||
|
|
||||||
it("adds normalized timestamps to pin payloads", async () => {
|
it("adds normalized timestamps to pin payloads", async () => {
|
||||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||||
listSlackPins.mockResolvedValueOnce([
|
listSlackPins.mockResolvedValueOnce([
|
||||||
|
|||||||
@@ -214,11 +214,13 @@ export async function handleSlackAction(
|
|||||||
typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined;
|
typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined;
|
||||||
const before = readStringParam(params, "before");
|
const before = readStringParam(params, "before");
|
||||||
const after = readStringParam(params, "after");
|
const after = readStringParam(params, "after");
|
||||||
|
const threadId = readStringParam(params, "threadId");
|
||||||
const result = await readSlackMessages(channelId, {
|
const result = await readSlackMessages(channelId, {
|
||||||
...readOpts,
|
...readOpts,
|
||||||
limit,
|
limit,
|
||||||
before: before ?? undefined,
|
before: before ?? undefined,
|
||||||
after: after ?? undefined,
|
after: after ?? undefined,
|
||||||
|
threadId: threadId ?? undefined,
|
||||||
});
|
});
|
||||||
const messages = result.messages.map((message) =>
|
const messages = result.messages.map((message) =>
|
||||||
withNormalizedTimestamp(
|
withNormalizedTimestamp(
|
||||||
|
|||||||
34
src/channels/plugins/slack.actions.test.ts
Normal file
34
src/channels/plugins/slack.actions.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { createSlackActions } from "./slack.actions.js";
|
||||||
|
|
||||||
|
const handleSlackAction = vi.fn(async () => ({ details: { ok: true } }));
|
||||||
|
|
||||||
|
vi.mock("../../agents/tools/slack-actions.js", () => ({
|
||||||
|
handleSlackAction: (...args: unknown[]) => handleSlackAction(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("slack actions adapter", () => {
|
||||||
|
it("forwards threadId for read", async () => {
|
||||||
|
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||||
|
const actions = createSlackActions("slack");
|
||||||
|
|
||||||
|
await actions.handleAction?.({
|
||||||
|
channel: "slack",
|
||||||
|
action: "read",
|
||||||
|
cfg,
|
||||||
|
params: {
|
||||||
|
channelId: "C1",
|
||||||
|
threadId: "171234.567",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||||
|
expect(params).toMatchObject({
|
||||||
|
action: "readMessages",
|
||||||
|
channelId: "C1",
|
||||||
|
threadId: "171234.567",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -133,6 +133,7 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap
|
|||||||
limit,
|
limit,
|
||||||
before: readStringParam(params, "before"),
|
before: readStringParam(params, "before"),
|
||||||
after: readStringParam(params, "after"),
|
after: readStringParam(params, "after"),
|
||||||
|
threadId: readStringParam(params, "threadId"),
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
},
|
},
|
||||||
cfg,
|
cfg,
|
||||||
|
|||||||
68
src/slack/actions.read.test.ts
Normal file
68
src/slack/actions.read.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
import { readSlackMessages } from "./actions.js";
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
return {
|
||||||
|
conversations: {
|
||||||
|
replies: vi.fn(async () => ({ messages: [], has_more: false })),
|
||||||
|
history: vi.fn(async () => ({ messages: [], has_more: false })),
|
||||||
|
},
|
||||||
|
} as unknown as WebClient & {
|
||||||
|
conversations: {
|
||||||
|
replies: ReturnType<typeof vi.fn>;
|
||||||
|
history: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("readSlackMessages", () => {
|
||||||
|
it("uses conversations.replies and drops the parent message", async () => {
|
||||||
|
const client = createClient();
|
||||||
|
client.conversations.replies.mockResolvedValueOnce({
|
||||||
|
messages: [{ ts: "171234.567" }, { ts: "171234.890" }, { ts: "171235.000" }],
|
||||||
|
has_more: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await readSlackMessages("C1", {
|
||||||
|
client,
|
||||||
|
threadId: "171234.567",
|
||||||
|
token: "xoxb-test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.conversations.replies).toHaveBeenCalledWith({
|
||||||
|
channel: "C1",
|
||||||
|
ts: "171234.567",
|
||||||
|
limit: undefined,
|
||||||
|
latest: undefined,
|
||||||
|
oldest: undefined,
|
||||||
|
});
|
||||||
|
expect(client.conversations.history).not.toHaveBeenCalled();
|
||||||
|
expect(result.messages.map((message) => message.ts)).toEqual(["171234.890", "171235.000"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses conversations.history when threadId is missing", async () => {
|
||||||
|
const client = createClient();
|
||||||
|
client.conversations.history.mockResolvedValueOnce({
|
||||||
|
messages: [{ ts: "1" }],
|
||||||
|
has_more: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await readSlackMessages("C1", {
|
||||||
|
client,
|
||||||
|
limit: 20,
|
||||||
|
token: "xoxb-test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.conversations.history).toHaveBeenCalledWith({
|
||||||
|
channel: "C1",
|
||||||
|
limit: 20,
|
||||||
|
latest: undefined,
|
||||||
|
oldest: undefined,
|
||||||
|
});
|
||||||
|
expect(client.conversations.replies).not.toHaveBeenCalled();
|
||||||
|
expect(result.messages.map((message) => message.ts)).toEqual(["1"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -186,9 +186,29 @@ export async function readSlackMessages(
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
before?: string;
|
before?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
|
threadId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<{ messages: SlackMessageSummary[]; hasMore: boolean }> {
|
): Promise<{ messages: SlackMessageSummary[]; hasMore: boolean }> {
|
||||||
const client = await getClient(opts);
|
const client = await getClient(opts);
|
||||||
|
|
||||||
|
// Use conversations.replies for thread messages, conversations.history for channel messages.
|
||||||
|
if (opts.threadId) {
|
||||||
|
const result = await client.conversations.replies({
|
||||||
|
channel: channelId,
|
||||||
|
ts: opts.threadId,
|
||||||
|
limit: opts.limit,
|
||||||
|
latest: opts.before,
|
||||||
|
oldest: opts.after,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
// conversations.replies includes the parent message; drop it for replies-only reads.
|
||||||
|
messages: (result.messages ?? []).filter(
|
||||||
|
(message) => (message as SlackMessageSummary)?.ts !== opts.threadId,
|
||||||
|
) as SlackMessageSummary[],
|
||||||
|
hasMore: Boolean(result.has_more),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const result = await client.conversations.history({
|
const result = await client.conversations.history({
|
||||||
channel: channelId,
|
channel: channelId,
|
||||||
limit: opts.limit,
|
limit: opts.limit,
|
||||||
|
|||||||
Reference in New Issue
Block a user