Files
PaperClipAI/server/src/services/chat-sdk-runtime.adapters.test.ts
T
DottaandPaperclip 6abeb67334 feat: add opt-in chat provider and data foundation (#13100)
Add dormant provider contracts, qualified patched adapters, tenant-scoped persistence and lifecycle ownership without activating chat routes. Preserve the experimental integration as dependent PR #13038.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-09 13:49:12 -05:00

3888 lines
126 KiB
TypeScript

import { createHmac, generateKeyPairSync } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { Attachment, Logger } from "chat";
import {
createChatSdkEndpointRuntime,
scopeMicrosoftTeamsEgress,
} from "./chat-sdk-runtime.js";
import type { ChatSdkRuntimeCallbacks } from "./chat-sdk-runtime.js";
import { splitTelegramPublicationText } from "./chat-publication-stream.js";
import type {
ChatSdkStatePersistence,
ChatSdkStateRecord,
} from "./chat-sdk-state.js";
const persistence: ChatSdkStatePersistence = {
async compareAndSet() {
return true;
},
async deleteIfVersion() {
return true;
},
async read() {
return null;
},
};
function capturingLogger(entries: unknown[]): Logger {
const logger: Logger = {
child() {
return logger;
},
debug(message, ...args) {
entries.push(["debug", message, ...args]);
},
error(message, ...args) {
entries.push(["error", message, ...args]);
},
info(message, ...args) {
entries.push(["info", message, ...args]);
},
warn(message, ...args) {
entries.push(["warn", message, ...args]);
},
};
return logger;
}
function memoryPersistence(): ChatSdkStatePersistence {
const rows = new Map<string, ChatSdkStateRecord>();
const keyFor = (input: {
companyId: string;
endpointId: string;
key: string;
}) => `${input.companyId}:${input.endpointId}:${input.key}`;
return {
async compareAndSet(input) {
const key = keyFor(input);
const current = rows.get(key) ?? null;
if ((current?.version ?? null) !== input.expectedVersion) return false;
rows.set(key, {
value: input.value,
expiresAt: input.expiresAt,
version: (current?.version ?? 0) + 1,
});
return true;
},
async deleteIfVersion(input) {
const key = keyFor(input);
if (rows.get(key)?.version !== input.expectedVersion) return false;
rows.delete(key);
return true;
},
async read(scope, key) {
return rows.get(keyFor({ ...scope, key })) ?? null;
},
};
}
function signedSlackEventRequest(
signingSecret: string,
payload: Record<string, unknown>,
): Request {
const body = JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1_000));
const signature = `v0=${createHmac("sha256", signingSecret)
.update(`v0:${timestamp}:${body}`)
.digest("hex")}`;
return new Request(
"https://paperclip.example/api/chat-webhooks/public/slack",
{
method: "POST",
headers: {
"content-type": "application/json",
"x-slack-request-timestamp": timestamp,
"x-slack-signature": signature,
},
body,
},
);
}
describe("Chat SDK published adapter integration", () => {
it("fails initialization when pinned Teams adapter internals drift", () => {
expect(() => scopeMicrosoftTeamsEgress({} as never)).toThrowError(
expect.objectContaining({
name: "TeamsAdapterCompatibilityError",
code: "CHAT_ADAPTER_COMPATIBILITY_ERROR",
}),
);
});
it("constructs an isolated endpoint runtime for every pinned provider package", () => {
const providerConfigs = [
{
provider: "slack" as const,
userName: "paperclip-agent",
credentials: { botToken: "xoxb-test", signingSecret: "secret" },
},
{
provider: "github" as const,
userName: "paperclip-agent[bot]",
credentials: { token: "github_pat_test", webhookSecret: "secret" },
},
{
provider: "microsoft-teams" as const,
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
{
provider: "telegram" as const,
userName: "paperclip_agent_bot",
credentials: { botToken: "123:test", secretToken: "secret" },
},
];
const runtimes = providerConfigs.map((providerConfig, index) =>
createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-1",
endpointId: `endpoint-${index}`,
logger: "silent",
persistence,
providerConfig,
}),
);
expect(runtimes.map((runtime) => runtime.provider)).toEqual([
"slack",
"github",
"microsoft-teams",
"telegram",
]);
expect(runtimes.map((runtime) => runtime.sdkAdapterKey)).toEqual([
"slack",
"github",
"teams",
"telegram",
]);
expect(
new Set(runtimes.map((runtime) => runtime.getProviderAdapter())).size,
).toBe(4);
});
it("bounds each GitHub API request with a fresh abort signal", async () => {
const providerFetch = vi.fn(
async (_input: string | URL | Request, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(init?.signal?.aborted).toBe(false);
return Response.json({ resources: {} });
},
);
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-github-timeout",
endpointId: "endpoint-github-timeout",
logger: "silent",
persistence,
providerConfig: {
provider: "github",
userName: "paperclip-agent[bot]",
credentials: { token: "github_pat_test", webhookSecret: "secret" },
},
});
try {
const adapter = runtime.getProviderAdapter() as unknown as {
octokit: { request(route: string): Promise<unknown> };
};
await adapter.octokit.request("GET /rate_limit");
await adapter.octokit.request("GET /rate_limit");
expect(providerFetch).toHaveBeenCalledTimes(2);
const firstSignal = providerFetch.mock.calls[0]?.[1]?.signal;
const secondSignal = providerFetch.mock.calls[1]?.[1]?.signal;
expect(firstSignal).not.toBe(secondSignal);
} finally {
await runtime.shutdown();
vi.unstubAllGlobals();
}
});
it("bounds Telegram Bot API calls while preserving a caller abort signal", async () => {
const providerFetch = vi.fn(
async (_input: string | URL | Request, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(init?.signal?.aborted).toBe(false);
return Response.json({ ok: true, result: { id: 123 } });
},
);
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-telegram-timeout",
endpointId: "endpoint-telegram-timeout",
logger: "silent",
persistence,
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
});
try {
const adapter = runtime.getProviderAdapter() as unknown as {
telegramFetch(
method: string,
payload?: Record<string, unknown>,
request?: { signal?: AbortSignal },
): Promise<unknown>;
};
const caller = new AbortController();
await adapter.telegramFetch("getMe", {}, { signal: caller.signal });
const effectiveSignal = providerFetch.mock.calls[0]?.[1]?.signal;
expect(effectiveSignal).not.toBe(caller.signal);
caller.abort();
expect(effectiveSignal?.aborted).toBe(true);
} finally {
await runtime.shutdown();
vi.unstubAllGlobals();
}
});
it("configures a bounded Teams HTTP client reused by scoped egress", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-timeout",
endpointId: "endpoint-teams-timeout",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
try {
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
api: {
http: {
options?: { timeout?: number };
http?: { defaults?: { timeout?: number } };
};
};
};
};
expect(
adapter.app.api.http.options?.timeout ??
adapter.app.api.http.http?.defaults?.timeout,
).toBe(45_000);
} finally {
await runtime.shutdown();
}
});
it("bounds adapter-owned Slack and Discord webhook fetches", async () => {
const providerFetch = vi.fn(
async (_input: string | URL | Request, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(init?.signal?.aborted).toBe(false);
return new Response("", { status: 200 });
},
);
vi.stubGlobal("fetch", providerFetch);
const slackRuntime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-fetch-timeout",
endpointId: "endpoint-slack-fetch-timeout",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: { botToken: "xoxb-test", signingSecret: "secret" },
},
});
const discordRuntime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-discord-fetch-timeout",
endpointId: "endpoint-discord-fetch-timeout",
logger: "silent",
persistence,
providerConfig: {
provider: "discord",
userName: "Paperclip Agent",
credentials: {
apiUrl: "https://discord.com/api/v10",
applicationId: "123456789012345678",
botToken: "discord-secret",
guildId: "1457808928258658549",
},
},
});
try {
const slack = slackRuntime.getProviderAdapter() as unknown as {
forwardSocketEvent(
webhookUrl: string,
event: { body: { type: string } },
): Promise<void>;
sendToResponseUrl(responseUrl: string, action: "delete"): Promise<void>;
};
const discord = discordRuntime.getProviderAdapter() as unknown as {
forwardGatewayEvent(
webhookUrl: string,
event: { type: string },
): Promise<void>;
};
await slack.sendToResponseUrl("https://paperclip.test/slack", "delete");
await slack.forwardSocketEvent("https://paperclip.test/slack", {
body: { type: "events_api" },
});
await discord.forwardGatewayEvent("https://paperclip.test/discord", {
type: "MESSAGE_CREATE",
});
expect(providerFetch).toHaveBeenCalledTimes(3);
} finally {
await Promise.all([slackRuntime.shutdown(), discordRuntime.shutdown()]);
vi.unstubAllGlobals();
}
});
it("refreshes an expired GitHub App installation token before a delayed send", async () => {
const start = new Date("2026-09-05T12:00:00.000Z");
vi.setSystemTime(start);
const privateKey = generateKeyPairSync("rsa", { modulusLength: 2048 })
.privateKey.export({ type: "pkcs8", format: "pem" })
.toString();
let tokenExchanges = 0;
const commentAuthorizations: string[] = [];
const providerFetch = vi.fn(
async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
const headers = new Headers(
input instanceof Request ? input.headers : init?.headers,
);
if (url.endsWith("/app/installations/2468/access_tokens")) {
tokenExchanges += 1;
return Response.json(
{
token: `ghs-installation-${tokenExchanges}`,
expires_at: new Date(Date.now() + 60 * 60_000).toISOString(),
permissions: { issues: "write", pull_requests: "write" },
repository_selection: "selected",
},
{ status: 201 },
);
}
if (url.endsWith("/repos/paperclipai/chat-e2e/issues/42/comments")) {
commentAuthorizations.push(headers.get("authorization") ?? "");
return Response.json(
{
id: 9_000 + commentAuthorizations.length,
body: "safe reply",
user: { id: 9001, login: "maya-paperclip[bot]", type: "Bot" },
},
{ status: 201 },
);
}
throw new Error(`Unexpected GitHub provider request: ${url}`);
},
);
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-github-token-refresh",
endpointId: "endpoint-github-token-refresh",
logger: "silent",
persistence,
providerConfig: {
provider: "github",
userName: "maya-paperclip[bot]",
credentials: {
appId: "123456",
botUserId: 9001,
installationId: 2468,
privateKey,
webhookSecret: "github-webhook-secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
postMessage(
threadId: string,
message: { markdown: string },
): Promise<{ id: string }>;
};
try {
await adapter.postMessage("github:paperclipai/chat-e2e:issue:42", {
markdown: "first safe reply",
});
vi.setSystemTime(new Date(start.getTime() + 61 * 60_000));
await adapter.postMessage("github:paperclipai/chat-e2e:issue:42", {
markdown: "delayed safe reply",
});
expect(tokenExchanges).toBe(2);
expect(commentAuthorizations).toHaveLength(2);
expect(commentAuthorizations[0]).toContain("ghs-installation-1");
expect(commentAuthorizations[1]).toContain("ghs-installation-2");
} finally {
await runtime.shutdown();
vi.useRealTimers();
vi.unstubAllGlobals();
}
});
it("preserves the pinned Slack Block Kit callback's clicked-message thread id", async () => {
const signingSecret = "slack-action-signing-secret";
const onAction = vi.fn();
const runtime = createChatSdkEndpointRuntime({
callbacks: { onAction, onMessage() {} },
companyId: "company-slack-action-envelope",
endpointId: "endpoint-slack-action-envelope",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret,
},
},
});
const payload = {
type: "block_actions",
team: { id: "T-PAPERCLIP" },
user: { id: "U-OPERATOR", username: "operator" },
channel: { id: "D-PAPERCLIP-DM" },
container: {
type: "message",
channel_id: "D-PAPERCLIP-DM",
message_ts: "1788.200",
},
message: { ts: "1788.200" },
actions: [{ action_id: "pcq:blue", value: "interaction-id" }],
trigger_id: "trigger-id",
};
const body = new URLSearchParams({
payload: JSON.stringify(payload),
}).toString();
const timestamp = String(Math.floor(Date.now() / 1_000));
const signature = `v0=${createHmac("sha256", signingSecret)
.update(`v0:${timestamp}:${body}`)
.digest("hex")}`;
try {
await runtime.initialize();
const response = await runtime.handleWebhook(
new Request(
"https://paperclip.example/api/chat-webhooks/public/slack",
{
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
"x-slack-request-timestamp": timestamp,
"x-slack-signature": signature,
},
body,
},
),
);
expect(response.status).toBe(200);
expect(onAction).toHaveBeenCalledWith(
expect.objectContaining({
provider: "slack",
event: expect.objectContaining({
actionId: "pcq:blue",
messageId: "1788.200",
threadId: "slack:D-PAPERCLIP-DM:1788.200",
value: "interaction-id",
}),
}),
);
} finally {
await runtime.shutdown();
}
});
it("acknowledges a signed Slack slash command without a blocking profile lookup", async () => {
const signingSecret = "slack-slash-signing-secret";
const onSlashCommand = vi.fn(async () => undefined);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {}, onSlashCommand },
companyId: "company-slack-slash-ack",
endpointId: "endpoint-slack-slash-ack",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret,
},
},
});
const body = new URLSearchParams({
channel_id: "C-PAPERCLIP",
command: "/paperclip-agent",
team_id: "T-PAPERCLIP",
text: "investigate the release",
trigger_id: "slash-trigger",
user_id: "U-OPERATOR",
user_name: "operator",
}).toString();
const timestamp = String(Math.floor(Date.now() / 1_000));
const signature = `v0=${createHmac("sha256", signingSecret)
.update(`v0:${timestamp}:${body}`)
.digest("hex")}`;
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: { users: { info(input: unknown): Promise<unknown> } };
};
const usersInfo = vi.fn(
async () =>
await new Promise<never>(() => {
// A regression to the upstream cold lookup would hold the provider
// acknowledgement open until Paperclip's webhook deadline.
}),
);
adapter._client.users.info = usersInfo;
const response = await Promise.race([
runtime.handleWebhook(
new Request(
"https://paperclip.example/api/chat-webhooks/public/slack",
{
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
"x-slack-request-timestamp": timestamp,
"x-slack-signature": signature,
},
body,
},
),
),
new Promise<"timed_out">((resolve) =>
setTimeout(() => resolve("timed_out"), 250),
),
]);
expect(response).not.toBe("timed_out");
expect((response as Response).status).toBe(200);
await expect((response as Response).json()).resolves.toEqual({
response_type: "ephemeral",
text: "Paperclip received this command.",
});
expect(usersInfo).not.toHaveBeenCalled();
expect(onSlashCommand).toHaveBeenCalledWith(
expect.objectContaining({
provider: "slack",
event: expect.objectContaining({
command: "/paperclip-agent",
text: "investigate the release",
user: expect.objectContaining({
userId: "U-OPERATOR",
userName: "operator",
fullName: "operator",
}),
}),
}),
);
} finally {
await runtime.shutdown();
}
});
it("parses signed Slack message edits and deletes through the pinned adapter", async () => {
const signingSecret = "slack-lifecycle-signing-secret";
const onMessageUpdated = vi.fn(async () => undefined);
const onMessageDeleted = vi.fn(async () => undefined);
const runtime = createChatSdkEndpointRuntime({
callbacks: {
onMessage() {},
onMessageDeleted,
onMessageUpdated,
},
companyId: "company-slack-lifecycle-envelope",
endpointId: "endpoint-slack-lifecycle-envelope",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret,
},
},
});
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
users: {
info(input: unknown): Promise<unknown>;
};
};
};
adapter._client.users.info = vi.fn(async () => ({
ok: true,
user: {
id: "U-OPERATOR",
name: "operator",
real_name: "Operator",
is_bot: false,
profile: { display_name: "operator", real_name: "Operator" },
},
}));
const previousMessage = {
type: "message",
user: "U-OPERATOR",
text: "@paperclip-agent original request",
ts: "1788.500",
thread_ts: "1788.400",
};
const edited = await runtime.handleWebhook(
signedSlackEventRequest(signingSecret, {
type: "event_callback",
team_id: "T-PAPERCLIP",
event_id: "Ev-slack-edit",
event: {
type: "message",
subtype: "message_changed",
channel: "C-PAPERCLIP",
channel_type: "channel",
event_ts: "1788.600",
message: {
...previousMessage,
text: "@paperclip-agent corrected request",
edited: { user: "U-OPERATOR", ts: "1788.600" },
},
previous_message: previousMessage,
},
}),
);
expect(edited.status).toBe(200);
expect(onMessageUpdated).toHaveBeenCalledOnce();
expect(onMessageUpdated).toHaveBeenCalledWith(
expect.objectContaining({
endpointId: "endpoint-slack-lifecycle-envelope",
provider: "slack",
thread: expect.objectContaining({
id: "slack:C-PAPERCLIP:1788.400",
}),
message: expect.objectContaining({
id: "1788.500",
text: expect.stringContaining("corrected request"),
}),
previousMessage: expect.objectContaining({
id: "1788.500",
text: expect.stringContaining("original request"),
}),
}),
);
const deleted = await runtime.handleWebhook(
signedSlackEventRequest(signingSecret, {
type: "event_callback",
team_id: "T-PAPERCLIP",
event_id: "Ev-slack-delete",
event: {
type: "message",
subtype: "message_deleted",
channel: "C-PAPERCLIP",
channel_type: "channel",
deleted_ts: "1788.500",
event_ts: "1788.700",
previous_message: previousMessage,
},
}),
);
expect(deleted.status).toBe(200);
expect(onMessageDeleted).toHaveBeenCalledOnce();
expect(onMessageDeleted).toHaveBeenCalledWith(
expect.objectContaining({
endpointId: "endpoint-slack-lifecycle-envelope",
provider: "slack",
event: expect.objectContaining({
channelId: "C-PAPERCLIP",
messageId: "1788.500",
threadId: "slack:C-PAPERCLIP:1788.400",
}),
}),
);
} finally {
await runtime.shutdown();
}
});
describe("signed Slack file-only message changes", () => {
const firstFile = {
id: "F-FIRST",
name: "first.png",
mimetype: "image/png",
size: 128,
original_w: 16,
original_h: 8,
url_private: "https://files.slack.com/files-pri/T-TEST/F-FIRST/first.png",
};
const secondFile = {
...firstFile,
id: "F-SECOND",
name: "second.png",
url_private:
"https://files.slack.com/files-pri/T-TEST/F-SECOND/second.png",
};
const unchangedFiles = [firstFile, secondFile];
const cases = [
{ name: "removal", files: [secondFile], changed: true },
{ name: "removal of all files", files: [], changed: true },
{ name: "removal with files omitted", files: undefined, changed: true },
{
name: "first file addition",
previousFiles: [],
files: [firstFile],
changed: true,
},
{
name: "absent versus empty files",
previousFiles: [],
files: undefined,
changed: false,
},
{
name: "addition",
files: [...unchangedFiles, { ...firstFile, id: "F-THIRD" }],
changed: true,
},
{
name: "replacement ID",
files: [{ ...firstFile, id: "F-REPLACEMENT" }, secondFile],
changed: true,
},
{ name: "reordering", files: [secondFile, firstFile], changed: true },
{
name: "name change",
files: [{ ...firstFile, name: "renamed.png" }, secondFile],
changed: true,
},
{
name: "MIME type change",
files: [{ ...firstFile, mimetype: "application/pdf" }, secondFile],
changed: true,
},
{
name: "size change",
files: [{ ...firstFile, size: 256 }, secondFile],
changed: true,
},
{
name: "width change",
files: [{ ...firstFile, original_w: 32 }, secondFile],
changed: true,
},
{
name: "height change",
files: [{ ...firstFile, original_h: 32 }, secondFile],
changed: true,
},
{
name: "non-hidden removal",
files: [secondFile],
changed: true,
hidden: false,
},
{
name: "identical files",
files: unchangedFiles.map((file) => ({ ...file })),
changed: false,
},
{
name: "private URL rotation",
files: [
{ ...firstFile, url_private: `${firstFile.url_private}?rotation=2` },
secondFile,
],
changed: false,
},
{
name: "unused title change",
files: [{ ...firstFile, title: "Different preview title" }, secondFile],
changed: false,
},
{
name: "unfurl-only update",
files: unchangedFiles,
changed: false,
unfurl: true,
},
{
name: "invalid webhook signature",
files: [secondFile],
changed: false,
invalidSignature: true,
},
];
it.each(cases)(
"handles $name without changing text or edit time",
async (testCase) => {
const signingSecret = "slack-file-lifecycle-signing-secret";
const onMessage = vi.fn<ChatSdkRuntimeCallbacks["onMessage"]>(
async () => undefined,
);
const onMessageUpdated = vi.fn<
NonNullable<ChatSdkRuntimeCallbacks["onMessageUpdated"]>
>(async () => undefined);
const onMessageDeleted = vi.fn(async () => undefined);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage, onMessageUpdated, onMessageDeleted },
companyId: "company-slack-file-lifecycle",
endpointId: "endpoint-slack-file-lifecycle",
logger: "silent",
persistence: memoryPersistence(),
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret,
},
},
});
const providerFetch = vi.fn(async () => {
throw new Error(
"Slack file lifecycle intake must not download files",
);
});
vi.stubGlobal("fetch", providerFetch);
const adapter = runtime.getProviderAdapter() as unknown as {
_client: { apiCall(...args: unknown[]): Promise<unknown> };
};
const providerApi = vi
.spyOn(adapter._client, "apiCall")
.mockRejectedValue(
new Error(
"Slack file lifecycle intake must not call provider APIs",
),
);
try {
await runtime.initialize();
const previousMessage = {
type: "message",
user: "U-OPERATOR",
username: "operator",
text: "Please inspect the attached images.",
ts: "1788.500",
thread_ts: "1788.400",
edited: { user: "U-OPERATOR", ts: "1788.600" },
files: testCase.previousFiles ?? unchangedFiles,
};
const response = await runtime.handleWebhook(
signedSlackEventRequest(
testCase.invalidSignature
? "wrong-signing-secret"
: signingSecret,
{
type: "event_callback",
team_id: "T-PAPERCLIP",
event_id: "Ev-slack-files-only",
event: {
type: "message",
subtype: "message_changed",
hidden: testCase.hidden ?? true,
channel: "C-PAPERCLIP",
channel_type: "channel",
event_ts: "1788.700",
message: {
...previousMessage,
files: testCase.files,
...(testCase.unfurl
? {
attachments: [
{
from_url: "https://example.com",
title: "Preview only",
},
],
}
: {}),
},
previous_message: previousMessage,
},
},
),
);
expect(response.status).toBe(testCase.invalidSignature ? 401 : 200);
expect(onMessageUpdated).toHaveBeenCalledTimes(
testCase.changed ? 1 : 0,
);
expect(onMessage).not.toHaveBeenCalled();
expect(onMessageDeleted).not.toHaveBeenCalled();
expect(providerApi).not.toHaveBeenCalled();
expect(providerFetch).not.toHaveBeenCalled();
if (testCase.changed) {
const update = onMessageUpdated.mock.calls[0]![0];
expect(update.provider).toBe("slack");
expect(update.thread.id).toBe("slack:C-PAPERCLIP:1788.400");
expect(update.message.id).toBe(previousMessage.ts);
expect(update.message.text).toBe(previousMessage.text);
expect(update.message.metadata.editedAt).toEqual(
new Date(1788.6 * 1_000),
);
expect((update.message.raw as { files?: unknown }).files).toEqual(
testCase.files,
);
expect(update.previousMessage?.raw).toEqual(
expect.objectContaining({ files: previousMessage.files }),
);
expect(update.message.attachments).toHaveLength(
testCase.files?.length ?? 0,
);
expect(update.previousMessage?.attachments).toHaveLength(
previousMessage.files.length,
);
for (const [index, file] of (testCase.files ?? []).entries()) {
expect(update.message.attachments[index]).toEqual(
expect.objectContaining({
name: file.name,
mimeType: file.mimetype,
size: file.size,
width: file.original_w,
height: file.original_h,
}),
);
}
}
} finally {
await runtime.shutdown();
providerApi.mockRestore();
vi.unstubAllGlobals();
}
},
);
});
it("parses signed Slack reaction add and remove events through the pinned adapter", async () => {
const signingSecret = "slack-reaction-signing-secret";
const onReaction = vi.fn<
NonNullable<ChatSdkRuntimeCallbacks["onReaction"]>
>(async () => undefined);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {}, onReaction },
companyId: "company-slack-reaction-envelope",
endpointId: "endpoint-slack-reaction-envelope",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret,
},
},
});
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
conversations: {
replies(input: unknown): Promise<unknown>;
};
users: {
info(input: unknown): Promise<unknown>;
};
};
};
adapter._client.conversations.replies = vi.fn(async () => ({
ok: true,
messages: [{ ts: "1788.500", thread_ts: "1788.400" }],
}));
adapter._client.users.info = vi.fn(async () => ({
ok: true,
user: {
id: "U-OPERATOR",
name: "operator",
real_name: "Operator",
is_bot: false,
profile: { display_name: "operator", real_name: "Operator" },
},
}));
for (const [type, eventTs] of [
["reaction_added", "1788.800"],
["reaction_removed", "1788.900"],
] as const) {
const response = await runtime.handleWebhook(
signedSlackEventRequest(signingSecret, {
type: "event_callback",
team_id: "T-PAPERCLIP",
event_id: `Ev-slack-${type}`,
event: {
type,
user: "U-OPERATOR",
reaction: "eyes",
item: {
type: "message",
channel: "C-PAPERCLIP",
ts: "1788.500",
},
event_ts: eventTs,
},
}),
);
expect(response.status).toBe(200);
}
expect(onReaction).toHaveBeenCalledTimes(2);
expect(onReaction.mock.calls.map(([callback]) => callback)).toEqual([
expect.objectContaining({
endpointId: "endpoint-slack-reaction-envelope",
provider: "slack",
event: expect.objectContaining({
added: true,
messageId: "1788.500",
rawEmoji: "eyes",
threadId: "slack:C-PAPERCLIP:1788.400",
}),
}),
expect.objectContaining({
endpointId: "endpoint-slack-reaction-envelope",
provider: "slack",
event: expect.objectContaining({
added: false,
messageId: "1788.500",
rawEmoji: "eyes",
threadId: "slack:C-PAPERCLIP:1788.400",
}),
}),
]);
} finally {
await runtime.shutdown();
}
});
it("falls back from unavailable Slack native streaming to bounded post and edit", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-stream-fallback",
endpointId: "endpoint-slack-stream-fallback",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "slack-stream-signing-secret",
},
},
});
try {
await runtime.initialize();
const nativeAppend = vi.fn(async () => {
throw Object.assign(new Error("native streaming unavailable"), {
code: "slack_webapi_platform_error",
data: { ok: false, error: "unknown_method" },
});
});
const nativeStop = vi.fn(async () => ({ ok: true, ts: "1788.999" }));
const chatStream = vi.fn(() => ({
append: nativeAppend,
stop: nativeStop,
}));
const postMessage = vi.fn(async () => ({
ok: true,
channel: "D-PAPERCLIP",
ts: "1788.901",
}));
const update = vi.fn(async () => ({
ok: true,
channel: "D-PAPERCLIP",
ts: "1788.901",
}));
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: {
postMessage: typeof postMessage;
update: typeof update;
};
chatStream: typeof chatStream;
};
endTyping(threadId: string): Promise<void>;
stream(
threadId: string,
chunks: AsyncIterable<string>,
options: {
recipientTeamId: string;
recipientUserId: string;
updateIntervalMs: number;
},
): Promise<{ id: string } | null>;
};
adapter._client.chatStream = chatStream;
adapter._client.chat.postMessage = postMessage;
adapter._client.chat.update = update;
adapter.endTyping = vi.fn(async () => undefined);
const chunks = async function* () {
yield "First safe paragraph.\n\n";
yield "Second safe paragraph.";
};
const sent = await adapter.stream(
"slack:D-PAPERCLIP:1788.400",
chunks(),
{
recipientTeamId: "T-PAPERCLIP",
recipientUserId: "U-OPERATOR",
updateIntervalMs: 0,
},
);
expect(sent?.id).toBe("1788.901");
expect(chatStream).toHaveBeenCalledOnce();
expect(nativeAppend).toHaveBeenCalledOnce();
expect(nativeStop).not.toHaveBeenCalled();
expect(postMessage).toHaveBeenCalledOnce();
expect(update).toHaveBeenCalledOnce();
expect(postMessage).toHaveBeenCalledWith(
expect.objectContaining({ text: "First safe paragraph.\n\n" }),
);
expect(update).toHaveBeenCalledWith(
expect.objectContaining({
text: "First safe paragraph.\n\nSecond safe paragraph.",
ts: "1788.901",
}),
);
} finally {
await runtime.shutdown();
}
});
it("uses one Slack provider call for a file-only publication", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-only",
endpointId: "endpoint-slack-file-only",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [
{
ok: true,
files: [
{
id: "FPAPERCLIP",
shares: {
public: {
"C-PAPERCLIP": [{ thread_ts: "1788.300", ts: "1788.302" }],
},
},
},
],
},
],
}));
const fileInfo = vi.fn();
const postMessage = vi.fn(async () => ({ ok: true, ts: "1788.301" }));
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: { info: typeof fileInfo; uploadV2: typeof uploadV2 };
};
postMessage(
threadId: string,
message: {
files: Array<{ data: Buffer; filename: string; mimeType: string }>;
markdown: string;
},
): Promise<{ id: string }>;
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
const sent = await adapter.postMessage("slack:C-PAPERCLIP:1788.300", {
markdown: "",
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
});
expect(sent.id).toBe("1788.302");
expect(uploadV2).toHaveBeenCalledOnce();
expect(uploadV2).toHaveBeenCalledWith(
expect.objectContaining({
channel_id: "C-PAPERCLIP",
thread_ts: "1788.300",
file_uploads: [expect.objectContaining({ filename: "result.txt" })],
}),
);
expect(fileInfo).not.toHaveBeenCalled();
expect(postMessage).not.toHaveBeenCalled();
} finally {
await runtime.shutdown();
}
});
it("resolves a sparse Slack upload response to its exact private thread share", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-identity",
endpointId: "endpoint-slack-file-identity",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [{ ok: true, files: [{ id: "FSPARSE" }] }],
}));
const fileInfo = vi
.fn()
.mockResolvedValueOnce({
ok: true,
file: { id: "FSPARSE", shares: {} },
})
.mockResolvedValueOnce({
ok: true,
file: {
id: "FSPARSE",
shares: {
private: {
"D-PAPERCLIP": [{ thread_ts: "1788.400", ts: "1788.401" }],
},
},
},
});
const postMessage = vi.fn();
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
postMessage(
threadId: string,
message: {
files: Array<{ data: Buffer; filename: string; mimeType: string }>;
markdown: string;
},
): Promise<{ id: string }>;
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
vi.useFakeTimers();
const sentPromise = adapter.postMessage("slack:D-PAPERCLIP:1788.400", {
markdown: "",
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
});
await vi.advanceTimersByTimeAsync(100);
const sent = await sentPromise;
expect(sent.id).toBe("1788.401");
expect(uploadV2).toHaveBeenCalledOnce();
expect(fileInfo).toHaveBeenCalledTimes(2);
expect(fileInfo).toHaveBeenCalledWith(
expect.objectContaining({ file: "FSPARSE" }),
);
expect(postMessage).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
await runtime.shutdown();
}
});
it("records accepted Slack file IDs before the share lookup and preserves Thread.post", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-receipt",
endpointId: "endpoint-slack-file-receipt",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [{ ok: true, files: [{ id: "FRECEIPT1" }] }],
}));
const fileInfo = vi.fn(async () => ({
ok: true,
file: {
id: "FRECEIPT1",
shares: {
public: {
"C-PAPERCLIP": [{ thread_ts: "1788.410", ts: "1788.411" }],
},
},
},
}));
const postMessage = vi.fn();
let releaseReceipt!: () => void;
const receiptReleased = new Promise<void>((resolve) => {
releaseReceipt = resolve;
});
let receiptEntered!: () => void;
const receiptAccepted = new Promise<void>((resolve) => {
receiptEntered = resolve;
});
const onUploadAccepted = vi.fn(async () => {
receiptEntered();
await receiptReleased;
});
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
const sentPromise = runtime.postSlackFilePublication(
"slack:C-PAPERCLIP:1788.410",
{
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
markdown: "",
},
onUploadAccepted,
);
await receiptAccepted;
expect(onUploadAccepted).toHaveBeenCalledWith({
version: 1,
channelId: "C-PAPERCLIP",
fileIds: ["FRECEIPT1"],
threadTs: "1788.410",
});
expect(fileInfo).not.toHaveBeenCalled();
releaseReceipt();
await expect(sentPromise).resolves.toMatchObject({
id: "1788.411",
threadId: "slack:C-PAPERCLIP:1788.410",
edit: expect.any(Function),
addReaction: expect.any(Function),
toJSON: expect.any(Function),
});
expect(uploadV2).toHaveBeenCalledOnce();
expect(fileInfo).toHaveBeenCalledOnce();
expect(postMessage).not.toHaveBeenCalled();
} finally {
releaseReceipt();
await runtime.shutdown();
}
});
it("isolates concurrent Slack upload receipt callbacks and restores the ordinary post context", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-concurrent",
endpointId: "endpoint-slack-file-concurrent",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
let releaseFirst!: () => void;
let releaseSecond!: () => void;
const firstReleased = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const secondReleased = new Promise<void>((resolve) => {
releaseSecond = resolve;
});
const uploadV2 = vi.fn(async (args: { thread_ts?: string }) => {
const first = args.thread_ts === "1788.430";
const second = args.thread_ts === "1788.440";
if (first) await firstReleased;
if (second) await secondReleased;
return {
ok: true,
files: [
{
ok: true,
files: [
{
id: first ? "FFIRST" : second ? "FSECOND" : "FORDINARY",
shares: {
public: {
"C-PAPERCLIP": [
{
thread_ts: args.thread_ts,
ts: first
? "1788.431"
: second
? "1788.441"
: "1788.451",
},
],
},
},
},
],
},
],
};
});
const fileInfo = vi.fn();
const postMessage = vi.fn();
const firstCallback = vi.fn(async () => undefined);
const secondCallback = vi.fn(async () => undefined);
const message = {
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
markdown: "",
};
const pending: Array<Promise<{ id: string }>> = [];
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: { info: typeof fileInfo; uploadV2: typeof uploadV2 };
};
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
const first = runtime.postSlackFilePublication(
"slack:C-PAPERCLIP:1788.430",
message,
firstCallback,
);
const second = runtime.postSlackFilePublication(
"slack:C-PAPERCLIP:1788.440",
message,
secondCallback,
);
pending.push(first, second);
await vi.waitFor(() => expect(uploadV2).toHaveBeenCalledTimes(2));
// Complete in reverse order while both asynchronous contexts are live.
releaseSecond();
await expect(second).resolves.toMatchObject({ id: "1788.441" });
expect(firstCallback).not.toHaveBeenCalled();
expect(secondCallback).toHaveBeenCalledExactlyOnceWith({
version: 1,
channelId: "C-PAPERCLIP",
threadTs: "1788.440",
fileIds: ["FSECOND"],
});
releaseFirst();
await expect(first).resolves.toMatchObject({ id: "1788.431" });
expect(firstCallback).toHaveBeenCalledExactlyOnceWith({
version: 1,
channelId: "C-PAPERCLIP",
threadTs: "1788.430",
fileIds: ["FFIRST"],
});
await expect(
runtime.thread("slack:C-PAPERCLIP:1788.450").post(message),
).resolves.toMatchObject({ id: "1788.451" });
expect(firstCallback).toHaveBeenCalledOnce();
expect(secondCallback).toHaveBeenCalledOnce();
expect(uploadV2).toHaveBeenCalledTimes(3);
expect(fileInfo).not.toHaveBeenCalled();
expect(postMessage).not.toHaveBeenCalled();
} finally {
releaseFirst();
releaseSecond();
await Promise.allSettled(pending);
await runtime.shutdown();
}
});
it.each(["receipt write", "malformed accepted ID"])(
"does not repeat a Slack upload or look up shares after a failed %s",
async (failure) => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-callback-failure",
endpointId: "endpoint-slack-file-callback-failure",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [
{
ok: true,
files: [
{
id:
failure === "receipt write" ? "FACCEPTED" : "invalid-file-id",
},
],
},
],
}));
const fileInfo = vi.fn();
const postMessage = vi.fn();
const onUploadAccepted = vi.fn(async () => {
throw new Error("receipt persistence failed");
});
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: { info: typeof fileInfo; uploadV2: typeof uploadV2 };
};
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
await expect(
runtime.postSlackFilePublication(
"slack:C-PAPERCLIP:1788.460",
{
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
markdown: "",
},
onUploadAccepted,
),
).rejects.toThrow();
expect(uploadV2).toHaveBeenCalledOnce();
expect(onUploadAccepted).toHaveBeenCalledTimes(
failure === "receipt write" ? 1 : 0,
);
expect(fileInfo).not.toHaveBeenCalled();
expect(postMessage).not.toHaveBeenCalled();
} finally {
await runtime.shutdown();
}
},
);
it("resolves a captured Slack file receipt with metadata reads only", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-recovery",
endpointId: "endpoint-slack-file-recovery",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn();
const fileInfo = vi.fn(async () => ({
ok: true,
file: {
id: "FRECOVERY1",
shares: {
public: {
"C-PAPERCLIP": [{ thread_ts: "1788.420", ts: "1788.421" }],
},
},
},
}));
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
await expect(
runtime.resolveSlackFileUploadReceipt("slack:C-PAPERCLIP:1788.420", [
"FRECOVERY1",
]),
).resolves.toBe("1788.421");
expect(fileInfo).toHaveBeenCalledOnce();
expect(uploadV2).not.toHaveBeenCalled();
await expect(
runtime.resolveSlackFileUploadReceipt("slack:C-PAPERCLIP:1788.420", [
"not-a-slack-file-id",
]),
).rejects.toMatchObject({ name: "ValidationError" });
expect(fileInfo).toHaveBeenCalledOnce();
} finally {
await runtime.shutdown();
}
});
it("rejects a Slack file upload whose provider message identity stays ambiguous", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-ambiguous",
endpointId: "endpoint-slack-file-ambiguous",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [{ ok: true, files: [{ id: "FAMBIGUOUS" }] }],
}));
const fileInfo = vi.fn(async () => ({
ok: true,
file: {
id: "FAMBIGUOUS",
shares: {
public: {
"C-OTHER": [{ ts: "1788.501" }],
},
},
},
}));
const postMessage = vi.fn();
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
postMessage(
threadId: string,
message: {
files: Array<{ data: Buffer; filename: string; mimeType: string }>;
markdown: string;
},
): Promise<{ id: string }>;
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
vi.useFakeTimers();
const rejected = expect(
adapter.postMessage("slack:C-PAPERCLIP:1788.500", {
markdown: "",
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
}),
).rejects.toMatchObject({
message:
"Slack file upload completed, but its message identity could not be confirmed",
});
await vi.advanceTimersByTimeAsync(5_000);
await rejected;
expect(uploadV2).toHaveBeenCalledOnce();
expect(fileInfo).toHaveBeenCalledTimes(8);
expect(postMessage).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
await runtime.shutdown();
}
});
it("bounds a stalled Slack file identity lookup without uploading again", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-timeout",
endpointId: "endpoint-slack-file-timeout",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [{ ok: true, files: [{ id: "FTIMEOUT" }] }],
}));
const fileInfo = vi.fn(() => new Promise<never>(() => undefined));
const postMessage = vi.fn();
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
postMessage(
threadId: string,
message: {
files: Array<{ data: Buffer; filename: string; mimeType: string }>;
markdown: string;
},
): Promise<{ id: string }>;
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
vi.useFakeTimers();
const rejected = expect(
adapter.postMessage("slack:C-PAPERCLIP:1788.700", {
markdown: "",
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
}),
).rejects.toMatchObject({
message:
"Slack file upload completed, but its message identity could not be confirmed",
});
await vi.advanceTimersByTimeAsync(5_000);
await rejected;
expect(uploadV2).toHaveBeenCalledOnce();
expect(fileInfo).toHaveBeenCalledOnce();
expect(postMessage).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
await runtime.shutdown();
}
});
it("does not start a Slack file lookup after token resolution exceeds the deadline", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-slack-file-late-token",
endpointId: "endpoint-slack-file-late-token",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [{ ok: true, files: [{ id: "FLATETOKEN" }] }],
}));
const fileInfo = vi.fn();
const postMessage = vi.fn();
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
postMessage(
threadId: string,
message: {
files: Array<{ data: Buffer; filename: string; mimeType: string }>;
markdown: string;
},
): Promise<{ id: string }>;
withToken<T extends object>(input: T): Promise<T>;
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
vi.useFakeTimers();
adapter.withToken = async <T extends object>(input: T): Promise<T> => {
await new Promise((resolve) => setTimeout(resolve, 6_000));
return input;
};
const rejected = expect(
adapter.postMessage("slack:C-PAPERCLIP:1788.800", {
markdown: "",
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
}),
).rejects.toMatchObject({
message:
"Slack file upload completed, but its message identity could not be confirmed",
});
await vi.advanceTimersByTimeAsync(5_000);
await rejected;
expect(uploadV2).toHaveBeenCalledOnce();
expect(fileInfo).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1_000);
expect(fileInfo).not.toHaveBeenCalled();
expect(postMessage).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
await runtime.shutdown();
}
});
it.each([
{
caseName: "omits the requested file",
infoResult: { ok: true },
},
{
caseName: "returns a different file",
infoResult: {
ok: true,
file: {
id: "FOTHER",
shares: {
public: {
"C-PAPERCLIP": [{ thread_ts: "1788.600", ts: "1788.601" }],
},
},
},
},
},
])(
"rejects a Slack file identity lookup that $caseName",
async ({ caseName, infoResult }) => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: `company-slack-file-${caseName}`,
endpointId: `endpoint-slack-file-${caseName}`,
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-PAPERCLIP-BOT",
signingSecret: "secret",
},
},
});
const uploadV2 = vi.fn(async () => ({
ok: true,
files: [{ ok: true, files: [{ id: "FEXPECTED" }] }],
}));
const fileInfo = vi.fn(async () => infoResult);
const postMessage = vi.fn();
try {
await runtime.initialize();
const adapter = runtime.getProviderAdapter() as unknown as {
_client: {
chat: { postMessage: typeof postMessage };
files: {
info: typeof fileInfo;
uploadV2: typeof uploadV2;
};
};
postMessage(
threadId: string,
message: {
files: Array<{
data: Buffer;
filename: string;
mimeType: string;
}>;
markdown: string;
},
): Promise<{ id: string }>;
};
adapter._client.files.uploadV2 = uploadV2;
adapter._client.files.info = fileInfo;
adapter._client.chat.postMessage = postMessage;
await expect(
adapter.postMessage("slack:C-PAPERCLIP:1788.600", {
markdown: "",
files: [
{
data: Buffer.from("safe artifact"),
filename: "result.txt",
mimeType: "text/plain",
},
],
}),
).rejects.toMatchObject({
message:
"Slack file upload completed, but its message identity could not be confirmed",
});
expect(fileInfo).toHaveBeenCalledOnce();
expect(postMessage).not.toHaveBeenCalled();
} finally {
await runtime.shutdown();
}
},
);
it("parses attachments carried by a Telegram slash-command caption", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-telegram-caption-file",
endpointId: "endpoint-telegram-caption-file",
logger: "silent",
persistence,
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
});
try {
const message = runtime.parseTelegramCommandMessage({
message_id: 14,
date: 1_788_700_000,
chat: { id: -1004415501660, type: "supergroup", title: "Agent Lab" },
from: { id: 417200359, is_bot: false, first_name: "Dotta" },
caption: "/task@paperclip_agent_bot inspect this file",
caption_entities: [{ offset: 0, length: 30, type: "bot_command" }],
document: {
file_id: "telegram-file-id",
file_unique_id: "telegram-unique-id",
file_name: "proof.txt",
mime_type: "text/plain",
file_size: 41,
},
});
expect(message?.attachments).toEqual([
expect.objectContaining({
type: "file",
name: "proof.txt",
mimeType: "text/plain",
size: 41,
fetchMetadata: {
fileId: "telegram-file-id",
fileUniqueId: "telegram-unique-id",
},
}),
]);
expect(
runtime.attachmentRecoveryDescriptor(message!.attachments[0]!),
).toEqual(
expect.objectContaining({
provider: "telegram",
locator: {
kind: "telegram_file_id",
fileId: "telegram-file-id",
fileUniqueId: "telegram-unique-id",
},
}),
);
} finally {
await runtime.shutdown();
}
});
it.each([
{ label: "an absent Content-Length", contentLength: undefined },
{ label: "a misleading small Content-Length", contentLength: "4" },
])(
"stops Telegram attachment downloads at Paperclip's byte cap with $label",
async ({ contentLength }) => {
const providerFetch = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith("/getFile")) {
return Response.json({
ok: true,
result: { file_path: "documents/bounded.bin" },
});
}
if (url.includes("/file/bot")) {
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3]));
controller.enqueue(new Uint8Array([4, 5, 6]));
controller.close();
},
}),
{
status: 200,
headers: contentLength
? { "content-length": contentLength }
: undefined,
},
);
}
throw new Error(`Unexpected Telegram URL: ${url}`);
});
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: `company-telegram-download-${contentLength ?? "absent"}`,
endpointId: `endpoint-telegram-download-${contentLength ?? "absent"}`,
logger: "silent",
persistence,
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
maxDownloadBytes: 5,
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
});
try {
const message = runtime.parseTelegramCommandMessage({
message_id: 15,
date: 1_788_700_001,
chat: { id: 77112233, type: "private" },
from: { id: 77112233, is_bot: false, first_name: "Dotta" },
caption: "/task inspect this file",
caption_entities: [{ offset: 0, length: 5, type: "bot_command" }],
document: {
file_id: "telegram-bounded-file",
file_name: "bounded.bin",
mime_type: "application/octet-stream",
},
});
await expect(
message!.attachments[0]!.fetchData!(),
).rejects.toMatchObject({
name: "NetworkError",
message: expect.stringContaining("exceeds the download limit"),
});
expect(providerFetch).toHaveBeenCalledTimes(2);
} finally {
await runtime.shutdown();
vi.unstubAllGlobals();
}
},
);
it("sends typed Telegram output through native photo, audio, video, and document methods", async () => {
const methods: string[] = [];
const providerFetch = vi.fn(
async (input: string | URL | Request, _init?: RequestInit) => {
const method = new URL(String(input)).pathname.split("/").at(-1)!;
methods.push(method);
return Response.json({
ok: true,
result: {
message_id: methods.length,
date: 1_788_700_002,
chat: { id: 77112233, type: "private" },
},
});
},
);
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-telegram-native-output",
endpointId: "endpoint-telegram-native-output",
logger: "silent",
persistence,
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
maxDownloadBytes: 10,
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
});
try {
const adapter = runtime.getProviderAdapter() as unknown as {
postMessage(
threadId: string,
message: {
attachments: Array<{
data: Buffer;
mimeType: string;
name: string;
type: "audio" | "file" | "image" | "video";
}>;
markdown: string;
},
): Promise<unknown>;
};
const cases = [
["image", "image/png", "result.png"],
["audio", "audio/mpeg", "result.mp3"],
["audio", "audio/mp4", "result.m4a"],
["video", "video/mp4", "result.mp4"],
["file", "text/plain", "result.txt"],
["file", "audio/ogg", "voice.ogg"],
["file", "audio/wav", "recording.wav"],
["file", "audio/webm", "recording.webm"],
["file", "video/webm", "clip.webm"],
["file", "video/quicktime", "clip.mov"],
["file", "video/x-m4v", "clip.m4v"],
] as const;
for (const [type, mimeType, name] of cases) {
const bytes = Buffer.from(`original:${mimeType}:\u0000exact bytes\n`);
await adapter.postMessage("telegram:77112233", {
markdown: `Shared ${name}.`,
attachments: [{ data: bytes, mimeType, name, type }],
});
const multipart = providerFetch.mock.calls.at(-1)?.[1]?.body;
expect(multipart).toBeInstanceOf(FormData);
const field =
type === "file" ? "document" : type === "image" ? "photo" : type;
const uploaded = (multipart as FormData).get(field);
expect(uploaded).toBeInstanceOf(File);
expect((uploaded as File).name).toBe(name);
expect((uploaded as File).type).toBe(mimeType);
expect(Buffer.from(await (uploaded as File).arrayBuffer())).toEqual(
bytes,
);
}
expect(methods).toEqual([
"sendPhoto",
"sendAudio",
"sendAudio",
"sendVideo",
"sendDocument",
...Array<string>(6).fill("sendDocument"),
]);
expect(
providerFetch.mock.calls.every(
([, init]) => init?.body instanceof FormData,
),
).toBe(true);
} finally {
await runtime.shutdown();
vi.unstubAllGlobals();
}
});
it.each([
{
label: "worst-case durable part",
source: "!".repeat(1_600),
rendered: "\\!".repeat(1_600),
},
{
label: "exact escaped ceiling",
source: "!".repeat(2_048),
rendered: "\\!".repeat(2_048),
},
{
label: "exact astral ceiling",
source: "🙂".repeat(2_048),
rendered: "🙂".repeat(2_048),
},
{
label: "medium prose",
source: "A useful answer. ".repeat(120).trim(),
rendered: "A useful answer\\. ".repeat(120).trim(),
},
])(
"keeps Telegram $label lossless after regular-message fallback",
async ({ source, rendered }) => {
const regularPayloads: Array<{ text?: string }> = [];
const providerFetch = vi.fn(
async (input: string | URL | Request, init?: RequestInit) => {
const method = new URL(String(input)).pathname.split("/").at(-1);
if (method === "sendRichMessage") {
return Response.json(
{
ok: false,
error_code: 400,
description: "Bad Request: method not found",
},
{ status: 400 },
);
}
if (method === "sendMessage") {
regularPayloads.push(
JSON.parse(String(init?.body)) as { text?: string },
);
return Response.json({
ok: true,
result: {
message_id: 1,
date: 1_788_700_003,
chat: { id: 77112233, type: "private" },
},
});
}
throw new Error(`Unexpected Telegram method: ${method}`);
},
);
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-telegram-durable-part",
endpointId: "endpoint-telegram-durable-part",
logger: "silent",
persistence,
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
});
try {
const adapter = runtime.getProviderAdapter() as unknown as {
postMessage(
threadId: string,
message: { markdown: string },
): Promise<unknown>;
};
expect(splitTelegramPublicationText(source)).toEqual([source]);
await adapter.postMessage("telegram:77112233", { markdown: source });
expect(regularPayloads).toHaveLength(1);
const sentText = regularPayloads[0]?.text;
expect(sentText).toBe(rendered);
expect(sentText!.length).toBeLessThanOrEqual(4_096);
expect(sentText!.replaceAll("\\", "")).toBe(source);
} finally {
await runtime.shutdown();
vi.unstubAllGlobals();
}
},
);
it.each(["personal", "channel", "groupChat"] as const)(
"Teams close text replaces only the selected progress activity without retaining card controls (%s)",
async (scope) => {
// Transport proof only: the service must independently select a currently
// owned plain progress lane and veto authored/uncertain final messages.
// Actual installed adapter, App.send and Connector API serialization;
// only the final HTTP methods are replaced. No live tenant/UI claim.
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-close-edit",
endpointId: `endpoint-teams-close-${scope}`,
logger: "silent",
persistence: memoryPersistence(),
providerConfig: {
provider: "microsoft-teams",
userName: "maya",
credentials: {
appId: "00000000-0000-4000-8000-000000000000",
appPassword: "synthetic-password",
},
},
});
const network = vi.fn(async () => {
throw new Error("Unexpected network");
});
vi.stubGlobal("fetch", network);
const adapter = runtime.getProviderAdapter();
const app = (
adapter as unknown as {
app: {
api: {
serviceUrl: string;
http: {
put(url: string, body: unknown): Promise<{ data: unknown }>;
};
};
activitySender: {
client: {
post(url: string, body: unknown): Promise<{ data: unknown }>;
};
};
};
}
).app;
const post = vi
.spyOn(app.activitySender.client, "post")
.mockResolvedValueOnce({ data: { id: "authored-answer" } })
.mockResolvedValueOnce({ data: { id: "owned-progress" } });
const put = vi
.spyOn(app.api.http, "put")
.mockResolvedValue({ data: { id: "owned-progress" } });
const conversation =
scope === "personal"
? "a:close-personal"
: "19:close-channel@thread.tacv2";
const route = "https://smba.trafficmanager.net/emea/";
const threadId = `teams:${Buffer.from(conversation).toString("base64url")}:${Buffer.from(route).toString("base64url")}:${scope}`;
const originalApi = app.api;
try {
await runtime.initialize();
await adapter.postMessage(threadId, {
markdown: "The actual completed answer must remain unchanged.",
});
await adapter.postMessage(threadId, {
card: {
type: "card",
title: "Working",
children: [
{
type: "actions",
children: [
{ type: "button", id: "synthetic-stop", label: "Stop" },
],
},
],
},
});
expect(JSON.stringify(post.mock.calls[1]?.[1])).toContain(
"Action.Submit",
);
expect(JSON.stringify(post.mock.calls[1]?.[1])).toContain(
"synthetic-stop",
);
const edited = await adapter.editMessage(threadId, "owned-progress", {
markdown:
"This chat conversation is closed. The Paperclip task remains available.",
});
expect(edited).toMatchObject({ id: "owned-progress", threadId });
expect(put).toHaveBeenCalledTimes(1);
expect(put.mock.calls[0]?.[0]).toBe(
`${route}v3/conversations/${conversation}/activities/owned-progress`,
);
const body = JSON.parse(JSON.stringify(put.mock.calls[0]?.[1]));
expect(body).toMatchObject({
type: "message",
textFormat: "markdown",
text: "This chat conversation is closed. The Paperclip task remains available.",
});
expect(body.attachments ?? []).toEqual([]);
expect(body.suggestedActions).toBeUndefined();
expect(JSON.stringify(body)).not.toMatch(
/Action\.Submit|synthetic-stop|Working|authored-answer/,
);
expect(post).toHaveBeenCalledTimes(2);
expect(app.api).toBe(originalApi);
expect(network).not.toHaveBeenCalled();
// An uncertain edit is surfaced; the adapter must not post a second
// message, edit a different ID, or fall back to the authored answer.
put.mockRejectedValueOnce(
Object.assign(new Error("Synthetic connection lost"), {
code: "ECONNRESET",
}),
);
await expect(
adapter.editMessage(threadId, "owned-progress", {
markdown: "This chat conversation is closed.",
}),
).rejects.toThrow();
expect(put).toHaveBeenCalledTimes(2);
expect(put.mock.calls[1]?.[0]).toBe(put.mock.calls[0]?.[0]);
expect(post).toHaveBeenCalledTimes(2);
expect(network).not.toHaveBeenCalled();
} finally {
await runtime.shutdown();
vi.restoreAllMocks();
vi.unstubAllGlobals();
}
},
);
it("parses verified Teams edit, delete, and restore envelopes through the public adapter contract", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-message-update",
endpointId: "endpoint-teams-message-update",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-4000-8000-000000000511",
appPassword: "secret",
appTenantId: "00000000-0000-4000-8000-000000000522",
appType: "SingleTenant",
},
},
});
const conversationId = "19:message-update@thread.tacv2;messageid=root-1";
const serviceUrl = "https://smba.trafficmanager.net/amer/";
const message = runtime.parseMicrosoftTeamsMessage({
id: "teams-message-1",
type: "messageUpdate",
text: "Corrected Teams request",
timestamp: "2026-09-06T14:01:00.000Z",
serviceUrl,
from: { id: "29:teams-user", name: "Teams User" },
conversation: {
id: conversationId,
conversationType: "channel",
tenantId: "00000000-0000-4000-8000-000000000522",
},
channelData: {
eventType: "editMessage",
tenant: { id: "00000000-0000-4000-8000-000000000522" },
},
});
expect(message).toMatchObject({
id: "teams-message-1",
text: "Corrected Teams request",
threadId: `teams:${Buffer.from(conversationId).toString("base64url")}`,
author: { userId: "29:teams-user", fullName: "Teams User" },
});
expect(message?.threadId).not.toContain(
Buffer.from(serviceUrl).toString("base64url"),
);
for (const lifecycle of [
{
type: "messageDelete",
eventType: "softDeleteMessage",
text: undefined,
},
{
type: "messageUpdate",
eventType: "undeleteMessage",
text: "Restored Teams request",
},
] as const) {
const parsed = runtime.parseMicrosoftTeamsMessage({
id: "teams-message-1",
type: lifecycle.type,
text: lifecycle.text,
timestamp: "2026-09-06T14:02:00.000Z",
serviceUrl,
from: { id: "29:teams-user", name: "Teams User" },
conversation: {
id: conversationId,
conversationType: "channel",
tenantId: "00000000-0000-4000-8000-000000000522",
},
channelData: {
eventType: lifecycle.eventType,
tenant: { id: "00000000-0000-4000-8000-000000000522" },
},
});
expect(parsed).toMatchObject({
id: "teams-message-1",
text: lifecycle.text ?? "",
threadId: message?.threadId,
author: { userId: "29:teams-user", fullName: "Teams User" },
});
}
await runtime.shutdown();
});
it.each([
{
status: 403,
description: "Forbidden: bot was blocked by the user",
expected: { name: "PermissionError", code: "PERMISSION_DENIED" },
},
{
status: 401,
description: "Unauthorized",
expected: { name: "AuthenticationError", code: "AUTH_FAILED" },
},
])(
"preserves Telegram Bot API $status as its scoped adapter error",
async ({ description, expected, status }) => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-telegram-errors",
endpointId: `endpoint-telegram-${status}`,
logger: "silent",
persistence,
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
postMessage(
threadId: string,
message: { markdown: string },
): Promise<unknown>;
};
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({ ok: false, error_code: status, description }),
{
status,
headers: { "content-type": "application/json" },
},
);
try {
await expect(
adapter.postMessage("telegram:77112233", {
markdown: "Safe Telegram response",
}),
).rejects.toMatchObject({ adapter: "telegram", ...expected });
} finally {
globalThis.fetch = originalFetch;
await runtime.shutdown();
}
},
);
it.each([
{
label: "JSON response body subCode",
providerCode: "MessageWritesBlocked",
rawError: Object.assign(new Error("Teams request was forbidden"), {
innerHttpError: {
statusCode: 403,
body: JSON.stringify({
error: {
code: "Forbidden",
innerError: {
subCode: "MessageWritesBlocked",
message: "sensitive-provider-detail-must-not-survive",
},
},
}),
},
}),
},
{
label: "structured response code",
providerCode: "ForbiddenOperationException",
rawError: Object.assign(new Error("Teams request was forbidden"), {
status: 403,
response: {
data: {
error: { code: "ForbiddenOperationException" },
},
},
}),
},
])(
"retains bounded Teams 403 metadata from a $label",
async ({ providerCode, rawError }) => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-errors",
endpointId: `endpoint-teams-${providerCode}`,
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-4000-8000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: { activitySender: { send: () => Promise<never> } };
postMessage(
threadId: string,
message: { markdown: string },
): Promise<unknown>;
};
adapter.app.activitySender.send = async () => {
throw rawError;
};
const threadId = `teams:${Buffer.from("19:blocked-thread@thread.tacv2").toString("base64url")}:${Buffer.from("https://smba.trafficmanager.net/amer/").toString("base64url")}:channel`;
try {
const error = await adapter
.postMessage(threadId, { markdown: "Safe Teams response" })
.catch((caught: unknown) => caught);
expect(error).toMatchObject({
name: "PermissionError",
adapter: "teams",
code: "PERMISSION_DENIED",
status: 403,
statusCode: 403,
subCode: providerCode,
details: {
providerStatus: 403,
providerSubCode: providerCode,
},
});
expect((error as { providerCodes: string[] }).providerCodes).toContain(
providerCode,
);
expect(JSON.stringify(error)).not.toContain(
"sensitive-provider-detail-must-not-survive",
);
} finally {
await runtime.shutdown();
}
},
);
it("keeps a Teams 401 as an endpoint authentication error", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-auth-error",
endpointId: "endpoint-teams-auth-error",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-4000-8000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: { activitySender: { send: () => Promise<never> } };
postMessage(
threadId: string,
message: { markdown: string },
): Promise<unknown>;
};
adapter.app.activitySender.send = async () => {
throw Object.assign(new Error("expired credential"), {
innerHttpError: { statusCode: 401 },
});
};
const threadId = `teams:${Buffer.from("19:auth-thread@thread.tacv2").toString("base64url")}:${Buffer.from("https://smba.trafficmanager.net/amer/").toString("base64url")}:channel`;
try {
await expect(
adapter.postMessage(threadId, { markdown: "Do not deliver" }),
).rejects.toMatchObject({
name: "AuthenticationError",
adapter: "teams",
code: "AUTH_FAILED",
});
} finally {
await runtime.shutdown();
}
});
it.each([
{
provider: "slack",
providerConfig: {
provider: "slack" as const,
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-test",
botUserId: "U-BOT",
signingSecret: "slack-signing-secret",
},
},
request: new Request("https://paperclip.test/webhook", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "event_callback" }),
}),
},
{
provider: "github",
providerConfig: {
provider: "github" as const,
userName: "paperclip-agent[bot]",
credentials: {
botUserId: 1,
token: "github_pat_test",
webhookSecret: "github-webhook-secret",
},
},
request: new Request("https://paperclip.test/webhook", {
method: "POST",
headers: {
"content-type": "application/json",
"x-github-event": "issue_comment",
},
body: JSON.stringify({ action: "created" }),
}),
},
{
provider: "telegram",
providerConfig: {
provider: "telegram" as const,
userName: "paperclip_agent_bot",
credentials: {
botToken: "123:test",
secretToken: "telegram-webhook-secret",
},
},
request: new Request("https://paperclip.test/webhook", {
method: "POST",
headers: {
"content-type": "application/json",
"x-telegram-bot-api-secret-token": "wrong-secret",
},
body: JSON.stringify({ update_id: 1 }),
}),
},
{
provider: "microsoft-teams",
providerConfig: {
provider: "microsoft-teams" as const,
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "teams-secret",
appTenantId: "11111111-1111-1111-1111-111111111111",
appType: "SingleTenant" as const,
},
},
request: new Request("https://paperclip.test/webhook", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "message", text: "forged" }),
}),
},
])(
"rejects an unauthenticated $provider webhook before dispatch",
async ({ providerConfig, request }) => {
const providerFetch = vi.fn(async (input: string | URL | Request) => {
const url = input instanceof Request ? input.url : String(input);
if (providerConfig.provider === "telegram" && url.endsWith("/getMe")) {
return Response.json({
ok: true,
result: {
id: 123,
is_bot: true,
first_name: "Paperclip Agent",
username: "paperclip_agent_bot",
},
});
}
throw new Error(`Unexpected provider request: ${url}`);
});
vi.stubGlobal("fetch", providerFetch);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-signature-test",
endpointId: `endpoint-${providerConfig.provider}`,
logger: "silent",
persistence,
providerConfig,
});
try {
const response = await runtime.handleWebhook(request);
expect(response.status).toBeGreaterThanOrEqual(400);
expect(response.status).toBeLessThan(500);
} finally {
await runtime.shutdown();
vi.unstubAllGlobals();
}
},
);
it("preserves safe HTTPS destinations from signed GitHub Markdown comments", async () => {
const webhookSecret = "github-link-preservation-secret";
const safeLink = "https://example.test/build/42?view=summary";
const safeImage =
"https://github.com/user-attachments/assets/11111111-2222-3333-4444-555555555555";
const existingBareUrl = "https://already-visible.example.test/log.txt";
const markdown = [
`@paperclip-agent inspect [the build](${safeLink}).`,
`![failure.log](${safeImage})`,
`Existing URL: ${existingBareUrl}`,
"[unsafe](javascript:alert(1))",
"![credentialed](https://user:password@example.test/private)",
].join("\n\n");
const payload = {
action: "created",
comment: {
id: 4242,
body: markdown,
created_at: "2026-09-04T12:00:00.000Z",
updated_at: "2026-09-04T12:00:00.000Z",
user: { id: 101, login: "operator", type: "User" },
},
issue: { number: 42 },
repository: {
id: 202,
name: "chat-e2e",
owner: { id: 303, login: "paperclipai", type: "Organization" },
},
sender: { id: 101, login: "operator", type: "User" },
};
const body = JSON.stringify(payload);
const signature = `sha256=${createHmac("sha256", webhookSecret)
.update(body)
.digest("hex")}`;
const onMessage = vi.fn(async (_event: unknown) => undefined);
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage },
companyId: "company-github-links",
endpointId: "endpoint-github-links",
logger: "silent",
persistence,
providerConfig: {
provider: "github",
userName: "paperclip-agent",
credentials: {
botUserId: 999,
token: "github_pat_test",
webhookSecret,
},
},
});
try {
const response = await runtime.handleWebhook(
new Request("https://paperclip.test/webhook", {
method: "POST",
headers: {
"content-type": "application/json",
"x-github-event": "issue_comment",
"x-hub-signature-256": signature,
},
body,
}),
);
expect(response.status).toBe(200);
expect(onMessage).toHaveBeenCalledTimes(1);
const event = onMessage.mock.calls[0]?.[0] as unknown as {
message: { text: string };
};
expect(event.message.text).toContain("the build");
expect(event.message.text).toContain(safeLink);
expect(event.message.text).toContain(safeImage);
expect(event.message.text).toContain(existingBareUrl);
expect(event.message.text.split(existingBareUrl)).toHaveLength(2);
expect(event.message.text).not.toContain("javascript:");
expect(event.message.text).not.toContain(
"https://user:password@example.test/private",
);
const adapter = runtime.getProviderAdapter() as unknown as {
parseReviewComment(
comment: Record<string, unknown>,
repository: Record<string, unknown>,
prNumber: number,
threadId: string,
): { text: string };
};
const review = adapter.parseReviewComment(
payload.comment,
payload.repository,
42,
"github:paperclipai/chat-e2e:42:rc:4242",
);
expect(review.text).toContain(safeLink);
expect(review.text).toContain(safeImage);
} finally {
await runtime.shutdown();
}
});
it("never logs unauthenticated Teams webhook bodies or authorization values", async () => {
const bodyMarker = "teams-body-secret-marker-7c6f";
const authorizationMarker = "teams-auth-secret-marker-94bd";
const logEntries: unknown[] = [];
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-log-boundary",
endpointId: "endpoint-teams-log-boundary",
logger: capturingLogger(logEntries),
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "teams-secret",
appTenantId: "11111111-1111-1111-1111-111111111111",
appType: "SingleTenant",
},
},
});
try {
const response = await runtime.handleWebhook(
new Request("https://paperclip.test/webhook", {
method: "POST",
headers: {
authorization: `Bearer ${authorizationMarker}`,
"content-type": "application/json",
},
body: JSON.stringify({
id: "forged-activity",
type: "message",
text: bodyMarker,
value: { privateInput: bodyMarker },
}),
}),
);
expect(response.status).toBeGreaterThanOrEqual(400);
expect(response.status).toBeLessThan(500);
const serializedLogs = JSON.stringify(logEntries);
expect(serializedLogs).not.toContain(bodyMarker);
expect(serializedLogs).not.toContain(authorizationMarker);
expect(serializedLogs).not.toContain("Teams webhook raw body");
} finally {
await runtime.shutdown();
}
});
it("round-trips credential-free Slack, Teams, and Telegram attachment recovery metadata", () => {
const cases: Array<{
attachment: Attachment;
credentials: string[];
expectedLocator: Record<string, unknown>;
providerConfig: Parameters<
typeof createChatSdkEndpointRuntime
>[0]["providerConfig"];
}> = [
{
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: {
botToken: "xoxb-never-persist",
signingSecret: "slack-signing-never-persist",
},
},
credentials: ["xoxb-never-persist", "slack-signing-never-persist"],
attachment: {
type: "image",
name: " release\u0000-plan.png ",
mimeType: "image/png",
size: 2048,
width: 640,
height: 480,
url: "https://files.slack.com/files-pri/T123-F123/release-plan.png",
fetchMetadata: {
url: "https://files.slack.com/files-pri/T123-F123/release-plan.png",
teamId: "T123",
botToken: "xoxb-never-persist",
arbitrary: "slack-private-metadata",
},
},
expectedLocator: {
kind: "slack_private_url",
teamId: "T123",
url: "https://files.slack.com/files-pri/T123-F123/release-plan.png",
},
},
{
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "teams-password-never-persist",
},
},
credentials: ["teams-password-never-persist"],
attachment: {
type: "file",
name: "design.docx",
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
size: 4096,
fetchMetadata: {
auth: "bot",
url: "https://smba.trafficmanager.net/amer/v3/attachments/A1/views/original",
connectorOrigin: "https://smba.trafficmanager.net",
clientSecret: "teams-password-never-persist",
},
},
expectedLocator: {
kind: "teams_bot_url",
url: "https://smba.trafficmanager.net/amer/v3/attachments/A1/views/original",
connectorOrigin: "https://smba.trafficmanager.net",
},
},
{
providerConfig: {
provider: "telegram",
userName: "paperclip_agent_bot",
credentials: {
botToken: "123:telegram-never-persist",
secretToken: "telegram-webhook-never-persist",
},
},
credentials: [
"123:telegram-never-persist",
"telegram-webhook-never-persist",
],
attachment: {
type: "video",
name: "demo.mp4",
mimeType: "video/mp4",
size: 8192,
width: 1280,
height: 720,
fetchMetadata: {
fileId: "telegram-file-id",
fileUniqueId: "telegram-stable-id",
botToken: "123:telegram-never-persist",
},
},
expectedLocator: {
kind: "telegram_file_id",
fileId: "telegram-file-id",
fileUniqueId: "telegram-stable-id",
},
},
];
for (const [index, testCase] of cases.entries()) {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-attachment-recovery",
endpointId: `endpoint-attachment-${index}`,
logger: "silent",
persistence,
providerConfig: testCase.providerConfig,
});
const descriptor = runtime.attachmentRecoveryDescriptor(
testCase.attachment,
);
expect(descriptor).toMatchObject({
version: 1,
provider: testCase.providerConfig.provider,
attachment: {
type: testCase.attachment.type,
name: testCase.attachment.name?.replace("\u0000", "").trim(),
mimeType: testCase.attachment.mimeType,
size: testCase.attachment.size,
},
locator: testCase.expectedLocator,
});
const persisted = JSON.parse(JSON.stringify(descriptor)) as unknown;
const serialized = JSON.stringify(persisted);
for (const credential of testCase.credentials) {
expect(serialized).not.toContain(credential);
}
expect(serialized).not.toContain("arbitrary");
expect(serialized).not.toContain("clientSecret");
expect(serialized).not.toContain("botToken");
const recovered = runtime.rehydrateAttachment(persisted);
expect(recovered).toMatchObject(descriptor?.attachment ?? {});
expect(recovered?.fetchMetadata).toBeDefined();
expect(recovered?.fetchData).toEqual(expect.any(Function));
}
});
it("fails closed for cross-provider and bearer-style Teams attachment locators", () => {
const teams = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-attachment-recovery",
endpointId: "endpoint-teams-attachment",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
const unsafe = teams.attachmentRecoveryDescriptor({
type: "file",
name: "private.docx",
fetchMetadata: {
url: "https://files.example.test/private.docx?access_token=secret",
},
});
expect(unsafe).toBeNull();
const slack = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-attachment-recovery",
endpointId: "endpoint-slack-attachment",
logger: "silent",
persistence,
providerConfig: {
provider: "slack",
userName: "paperclip-agent",
credentials: { botToken: "token", signingSecret: "secret" },
},
});
expect(
slack.rehydrateAttachment({
version: 1,
provider: "telegram",
attachment: { type: "file" },
locator: { kind: "telegram_file_id", fileId: "file-id" },
}),
).toBeNull();
});
it("isolates concurrent Teams sends by verified thread service URL", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-egress",
endpointId: "endpoint-teams-egress",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
activitySender: {
send: (
activity: unknown,
reference: { serviceUrl: string },
) => Promise<{ id: string }>;
};
api: { serviceUrl: string };
};
postMessage: (
threadId: string,
message: { markdown: string },
) => Promise<unknown>;
};
const originalApi = adapter.app.api;
const observedServiceUrls: string[] = [];
let releaseFirst!: () => void;
const firstBlocked = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
let markFirstObserved!: () => void;
const firstObserved = new Promise<void>((resolve) => {
markFirstObserved = resolve;
});
let markSecondObserved!: () => void;
const secondObserved = new Promise<void>((resolve) => {
markSecondObserved = resolve;
});
adapter.app.activitySender.send = async (_activity, reference) => {
observedServiceUrls.push(reference.serviceUrl);
if (observedServiceUrls.length === 1) {
markFirstObserved();
await firstBlocked;
} else if (observedServiceUrls.length === 2) {
markSecondObserved();
}
return { id: "teams-outbound-1" };
};
const amerServiceUrl = "https://smba.trafficmanager.net/amer/";
const emeaServiceUrl = "https://smba.trafficmanager.net/emea/";
const amerThreadId = `teams:${Buffer.from("19:regional-thread@thread.tacv2;messageid=1729").toString("base64url")}:${Buffer.from(amerServiceUrl).toString("base64url")}:channel`;
const emeaThreadId = `teams:${Buffer.from("19:second-region@thread.tacv2;messageid=1730").toString("base64url")}:${Buffer.from(emeaServiceUrl).toString("base64url")}:channel`;
const first = adapter.postMessage(amerThreadId, {
markdown: "Americas response",
});
await firstObserved;
const second = adapter.postMessage(emeaThreadId, {
markdown: "EMEA response",
});
await secondObserved;
expect(observedServiceUrls).toEqual([
"https://smba.trafficmanager.net/amer",
"https://smba.trafficmanager.net/emea",
]);
releaseFirst();
await Promise.all([first, second]);
expect(observedServiceUrls).toEqual([
"https://smba.trafficmanager.net/amer",
"https://smba.trafficmanager.net/emea",
]);
expect(adapter.app.api).toBe(originalApi);
});
it("keeps Teams thread identity canonical while the latest durable route wins over legacy URLs", async () => {
const state = memoryPersistence();
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-route-refresh",
endpointId: "endpoint-teams-route-refresh",
logger: "silent",
persistence: state,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
await runtime.handleWebhook(
new Request("https://paperclip.test/webhook", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "message", text: "initialize" }),
}),
);
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
activitySender: {
send(
activity: unknown,
reference: { serviceUrl: string },
): Promise<{ id: string }>;
};
};
postMessage(
threadId: string,
message: { markdown: string },
): Promise<unknown>;
};
const conversationId =
"19:route-refresh@thread.tacv2;messageid=canonical-root";
const canonicalThreadId = `teams:${Buffer.from(conversationId).toString("base64url")}`;
const staleServiceUrl = "https://smba.trafficmanager.net/amer";
const currentServiceUrl = "https://smba.trafficmanager.net/emea";
const legacyThreadId = `${canonicalThreadId}:${Buffer.from(staleServiceUrl).toString("base64url")}`;
const observedServiceUrls: string[] = [];
adapter.app.activitySender.send = async (_activity, reference) => {
observedServiceUrls.push(reference.serviceUrl);
return { id: `sent-${observedServiceUrls.length}` };
};
await runtime.recordMicrosoftTeamsRoute(
canonicalThreadId,
currentServiceUrl,
);
await adapter.postMessage(legacyThreadId, { markdown: "legacy row" });
await adapter.postMessage(canonicalThreadId, { markdown: "canonical row" });
expect(observedServiceUrls).toEqual([currentServiceUrl, currentServiceUrl]);
await runtime.shutdown();
});
it("persists Teams metadata only after Paperclip admits the authenticated activity", async () => {
const state = memoryPersistence();
const tenantId = "00000000-0000-4000-8000-000000000622";
const appId = "00000000-0000-4000-8000-000000000611";
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-admitted-metadata",
endpointId: "endpoint-teams-admitted-metadata",
logger: "silent",
persistence: state,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId,
appPassword: "secret",
appTenantId: tenantId,
appType: "SingleTenant",
},
},
});
await runtime.handleWebhook(
new Request("https://paperclip.test/webhook", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "message", text: "initialize" }),
}),
);
const adapter = runtime.getProviderAdapter() as unknown as {
cacheUserContext(activity: unknown): void;
chat: {
getState(): {
get<T = unknown>(key: string): Promise<T | null>;
};
};
getIncomingUser(...args: unknown[]): Promise<unknown>;
getUser(...args: unknown[]): Promise<unknown>;
};
const userId = "29:admitted-teams-user";
const aadObjectId = "00000000-0000-4000-8000-000000000633";
const conversationId =
"19:admitted-channel@thread.tacv2;messageid=admitted-root";
const baseConversationId = "19:admitted-channel@thread.tacv2";
const serviceUrl = "https://smba.trafficmanager.net/amer/";
const raw = {
type: "message",
serviceUrl,
from: { id: userId, aadObjectId, name: "Admitted Teams User" },
conversation: {
id: conversationId,
conversationType: "channel",
tenantId,
},
channelData: {
tenant: { id: tenantId },
team: { aadGroupId: "admitted-team-aad-id" },
channel: { id: baseConversationId },
},
};
const sdkState = adapter.chat.getState();
adapter.cacheUserContext(raw);
expect(await adapter.getIncomingUser({}, userId, aadObjectId)).toBeNull();
expect(await adapter.getUser(userId)).toBeNull();
expect(await sdkState.get(`teams:serviceUrl:${userId}`)).toBeNull();
expect(await sdkState.get(`teams:aadObjectId:${userId}`)).toBeNull();
expect(await sdkState.get(`teams:tenantId:${userId}`)).toBeNull();
expect(
await sdkState.get(`teams:channelContext:${baseConversationId}`),
).toBeNull();
expect(runtime.acceptsProviderScope(raw)).toBe(true);
expect(
runtime.acceptsProviderScope({
...raw,
recipient: { id: appId, isTargeted: true },
}),
).toBe(false);
expect(
runtime.acceptsProviderScope({
...raw,
conversation: {
...raw.conversation,
tenantId: "00000000-0000-4000-8000-000000000699",
},
}),
).toBe(false);
const threadId = `teams:${Buffer.from(conversationId).toString("base64url")}`;
await runtime.recordMicrosoftTeamsRoute(threadId, serviceUrl, raw);
expect(await sdkState.get(`teams:serviceUrl:${userId}`)).toBe(
"https://smba.trafficmanager.net/amer",
);
expect(await sdkState.get(`teams:aadObjectId:${userId}`)).toBe(aadObjectId);
expect(await sdkState.get(`teams:tenantId:${userId}`)).toBe(tenantId);
expect(
JSON.parse(
String(
await sdkState.get(`teams:channelContext:${baseConversationId}`),
),
),
).toEqual({
teamId: "admitted-team-aad-id",
channelId: baseConversationId,
});
await runtime.shutdown();
});
it("scopes direct Teams edit, reaction, and delete calls to signed Microsoft service URLs", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-direct-api",
endpointId: "endpoint-teams-direct-api",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
api: unknown;
activitySender: {
send(
activity: unknown,
reference: { serviceUrl: string },
): Promise<{ id: string }>;
};
};
addReaction: (
threadId: string,
messageId: string,
emoji: string,
) => Promise<void>;
deleteMessage: (threadId: string, messageId: string) => Promise<void>;
editMessage: (
threadId: string,
messageId: string,
message: { markdown: string },
) => Promise<unknown>;
removeReaction: (
threadId: string,
messageId: string,
emoji: string,
) => Promise<void>;
};
const observed: Array<{ operation: string; serviceUrl: string }> = [];
class InstrumentedApi {
readonly conversations;
readonly _apiClientSettings: unknown;
readonly http: unknown;
constructor(
readonly serviceUrl: string,
http: unknown,
settings?: unknown,
) {
this.http = http;
this._apiClientSettings = settings;
this.conversations = {
activities: () => ({
update: async () => {
observed.push({ operation: "edit", serviceUrl });
},
delete: async () => {
observed.push({ operation: "delete", serviceUrl });
},
}),
addReaction: async () => {
observed.push({ operation: "add-reaction", serviceUrl });
},
deleteReaction: async () => {
observed.push({ operation: "remove-reaction", serviceUrl });
},
};
}
}
adapter.app.api = new InstrumentedApi(
"https://smba.trafficmanager.net/teams",
{},
);
const gccHigh = "https://smba.infra.gov.teams.microsoft.us/teams";
const dod = "https://smba.infra.dod.teams.microsoft.us/teams";
const gccHighThread = `teams:${Buffer.from("19:gcc-high@thread.tacv2;messageid=2001").toString("base64url")}:${Buffer.from(gccHigh).toString("base64url")}:channel`;
const dodThread = `teams:${Buffer.from("19:dod@thread.tacv2;messageid=2002").toString("base64url")}:${Buffer.from(dod).toString("base64url")}:channel`;
await Promise.all([
adapter.editMessage(gccHighThread, "2001", { markdown: "updated" }),
adapter.addReaction(dodThread, "2002", "eyes"),
adapter.removeReaction(gccHighThread, "2001", "eyes"),
adapter.deleteMessage(dodThread, "2002"),
]);
expect(observed).toEqual(
expect.arrayContaining([
{ operation: "edit", serviceUrl: gccHigh },
{ operation: "add-reaction", serviceUrl: dod },
{ operation: "remove-reaction", serviceUrl: gccHigh },
{ operation: "delete", serviceUrl: dod },
]),
);
expect(observed).toHaveLength(4);
});
it("isolates concurrent Teams openDM calls by cached user service URL", async () => {
const state = memoryPersistence();
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-open-dm",
endpointId: "endpoint-teams-open-dm",
logger: "silent",
persistence: state,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
appTenantId: "11111111-1111-1111-1111-111111111111",
appType: "SingleTenant",
},
},
});
await runtime.handleWebhook(
new Request("https://paperclip.test/webhook", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "message", text: "initialize" }),
}),
);
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
api: unknown;
activitySender: {
send(
activity: unknown,
reference: { serviceUrl: string },
): Promise<{ id: string }>;
};
};
chat: {
getState(): {
set(key: string, value: string, ttlMs?: number): Promise<void>;
};
};
openDM(userId: string): Promise<string>;
postMessage(
threadId: string,
message: { markdown: string },
): Promise<unknown>;
};
const amer = "https://smba.trafficmanager.net/amer";
const gcc = "https://smba.infra.gcc.teams.microsoft.com/teams";
await Promise.all([
adapter.chat
.getState()
.set("teams:serviceUrl:29:user-amer", amer, 60_000),
adapter.chat.getState().set("teams:serviceUrl:29:user-gcc", gcc, 60_000),
]);
const observedServiceUrls: string[] = [];
let releaseFirst!: () => void;
const firstBlocked = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
let markFirstObserved!: () => void;
const firstObserved = new Promise<void>((resolve) => {
markFirstObserved = resolve;
});
let markSecondObserved!: () => void;
const secondObserved = new Promise<void>((resolve) => {
markSecondObserved = resolve;
});
class InstrumentedOpenDmApi {
readonly conversations;
readonly _apiClientSettings: unknown;
readonly http: unknown;
constructor(
readonly serviceUrl: string,
http: unknown,
settings?: unknown,
) {
this.http = http;
this._apiClientSettings = settings;
this.conversations = {
create: async () => {
observedServiceUrls.push(serviceUrl);
const index = observedServiceUrls.length;
if (index === 1) {
markFirstObserved();
await firstBlocked;
} else if (index === 2) {
markSecondObserved();
}
return { id: `conversation-${index}` };
},
};
}
}
adapter.app.api = new InstrumentedOpenDmApi(
"https://smba.trafficmanager.net/teams",
{},
);
const first = adapter.openDM("29:user-amer");
await firstObserved;
const second = adapter.openDM("29:user-gcc");
await secondObserved;
expect(observedServiceUrls).toEqual([amer, gcc]);
releaseFirst();
const [amerThread, gccThread] = await Promise.all([first, second]);
expect(amerThread).toBe(
`teams:${Buffer.from("conversation-1").toString("base64url")}`,
);
expect(gccThread).toBe(
`teams:${Buffer.from("conversation-2").toString("base64url")}`,
);
observedServiceUrls.length = 0;
adapter.app.activitySender.send = async (_activity, reference) => {
observedServiceUrls.push(reference.serviceUrl);
return { id: `sent-${observedServiceUrls.length}` };
};
await adapter.postMessage(amerThread, { markdown: "Americas follow-up" });
await adapter.postMessage(gccThread, { markdown: "GCC follow-up" });
expect(observedServiceUrls).toEqual([amer, gcc]);
});
it("restores the Teams API client after a scoped outbound failure", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-egress-failure",
endpointId: "endpoint-teams-egress-failure",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
activitySender: {
send: () => Promise<never>;
};
api: { serviceUrl: string };
};
postMessage: (
threadId: string,
message: { markdown: string },
) => Promise<unknown>;
};
const originalApi = adapter.app.api;
adapter.app.activitySender.send = async () => {
throw new Error("simulated regional send failure");
};
const threadId = `teams:${Buffer.from("19:failed-thread@thread.tacv2;messageid=1730").toString("base64url")}:${Buffer.from("https://smba.trafficmanager.net/emea/").toString("base64url")}:channel`;
await expect(
adapter.postMessage(threadId, { markdown: "Failure response" }),
).rejects.toThrow("simulated regional send failure");
expect(adapter.app.api).toBe(originalApi);
});
it("accepts canonical Microsoft first-party Teams service URL suffixes", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-egress-first-party",
endpointId: "endpoint-teams-egress-first-party",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-4000-8000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
activitySender: {
send: (
activity: unknown,
reference: { serviceUrl: string },
) => Promise<{ id: string }>;
};
};
postMessage: (
threadId: string,
message: { markdown: string },
) => Promise<unknown>;
};
const observed: string[] = [];
adapter.app.activitySender.send = async (_activity, reference) => {
observed.push(reference.serviceUrl);
return { id: `first-party-${observed.length}` };
};
const serviceUrls = [
"https://skype.botframework.com/",
"https://regional.botframework.com/teams/",
"https://smba.trafficmanager.net/amer-client-ss.msg/",
"https://smba.infra.gcc.teams.microsoft.com/teams/",
"https://smba.infra.gov.teams.microsoft.us/teams/",
];
for (const [index, serviceUrl] of serviceUrls.entries()) {
const threadId = `teams:${Buffer.from(`19:first-party-${index}@thread.tacv2`).toString("base64url")}:${Buffer.from(serviceUrl).toString("base64url")}:channel`;
await adapter.postMessage(threadId, { markdown: "Safe response" });
}
expect(observed).toEqual(serviceUrls.map((url) => url.replace(/\/$/, "")));
});
it("rejects unsafe Teams thread service URLs before transport", async () => {
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-egress-untrusted",
endpointId: "endpoint-teams-egress-untrusted",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
activitySender: { send: () => Promise<{ id: string }> };
};
postMessage: (
threadId: string,
message: { markdown: string },
) => Promise<unknown>;
};
let transportCalled = false;
adapter.app.activitySender.send = async () => {
transportCalled = true;
return { id: "unexpected" };
};
const unsafeServiceUrls = [
"http://127.0.0.1:1234/internal",
"https://127.0.0.1/internal",
"https://smba.trafficmanager.net.attacker.example/teams",
"https://botframework.com.attacker.example/teams",
"https://attackerbotframework.com/teams",
"https://smba.trafficmanager.net:8443/teams",
"https://user@skype.botframework.com/teams",
"https://skype.botframework.com/teams/v3",
"https://skype.botframework.com/teams///",
"https://skype.botframework.com/%2e%2e/teams",
"https://skype.botframework.com/teams?redirect=attacker",
"https://skype.botframework.com/teams#fragment",
];
for (const serviceUrl of unsafeServiceUrls) {
const threadId = `teams:${Buffer.from("19:unsafe-thread@thread.tacv2").toString("base64url")}:${Buffer.from(serviceUrl).toString("base64url")}:channel`;
await expect(
adapter.postMessage(threadId, { markdown: "Do not send" }),
).rejects.toMatchObject({
name: "TeamsServiceUrlValidationError",
code: "CHAT_PROVIDER_PRETRANSPORT_REJECTED",
});
}
expect(transportCalled).toBe(false);
});
it("allows only the exact explicitly configured custom Teams API URL", async () => {
const configuredApiUrl = "https://connector.example.test:8443/custom";
const runtime = createChatSdkEndpointRuntime({
callbacks: { onMessage() {} },
companyId: "company-teams-custom-api",
endpointId: "endpoint-teams-custom-api",
logger: "silent",
persistence,
providerConfig: {
provider: "microsoft-teams",
userName: "Paperclip Agent",
credentials: {
apiUrl: configuredApiUrl,
appId: "00000000-0000-0000-0000-000000000000",
appPassword: "secret",
},
},
});
const adapter = runtime.getProviderAdapter() as unknown as {
app: {
activitySender: {
send: (
activity: unknown,
reference: { serviceUrl: string },
) => Promise<{ id: string }>;
};
};
postMessage: (
threadId: string,
message: { markdown: string },
) => Promise<unknown>;
};
const observed: string[] = [];
adapter.app.activitySender.send = async (_activity, reference) => {
observed.push(reference.serviceUrl);
return { id: "custom-api-message" };
};
const encodedConversation = Buffer.from(
"19:custom-api@thread.tacv2",
).toString("base64url");
const exactThread = `teams:${encodedConversation}:${Buffer.from(`${configuredApiUrl}/`).toString("base64url")}:channel`;
const otherPathThread = `teams:${encodedConversation}:${Buffer.from("https://connector.example.test:8443/other").toString("base64url")}:channel`;
await adapter.postMessage(exactThread, { markdown: "Configured route" });
await expect(
adapter.postMessage(otherPathThread, { markdown: "Wrong route" }),
).rejects.toMatchObject({
code: "CHAT_PROVIDER_PRETRANSPORT_REJECTED",
});
expect(observed).toEqual([configuredApiUrl]);
});
});