test: dedupe channel and transport adapters

This commit is contained in:
Peter Steinberger
2026-02-21 21:43:18 +00:00
parent 52ddb6ae18
commit 58254b3b57
19 changed files with 2187 additions and 2545 deletions

View File

@@ -204,42 +204,50 @@ describe("roundtrip encoding", () => {
// ─── extractDiscordChannelId ──────────────────────────────────────────────────
describe("extractDiscordChannelId", () => {
it("extracts channel ID from standard session key", () => {
expect(extractDiscordChannelId("agent:main:discord:channel:123456789")).toBe("123456789");
});
it("extracts channel IDs and rejects invalid session key inputs", () => {
const cases: Array<{
name: string;
input: string | null | undefined;
expected: string | null;
}> = [
{
name: "standard session key",
input: "agent:main:discord:channel:123456789",
expected: "123456789",
},
{
name: "agent-specific session key",
input: "agent:test-agent:discord:channel:999888777",
expected: "999888777",
},
{
name: "group session key",
input: "agent:main:discord:group:222333444",
expected: "222333444",
},
{
name: "longer session key",
input: "agent:my-agent:discord:channel:111222333:thread:444555",
expected: "111222333",
},
{
name: "non-discord session key",
input: "agent:main:telegram:channel:123456789",
expected: null,
},
{
name: "missing channel/group segment",
input: "agent:main:discord:dm:123456789",
expected: null,
},
{ name: "null input", input: null, expected: null },
{ name: "undefined input", input: undefined, expected: null },
{ name: "empty input", input: "", expected: null },
];
it("extracts channel ID from agent session key", () => {
expect(extractDiscordChannelId("agent:test-agent:discord:channel:999888777")).toBe("999888777");
});
it("extracts channel ID from group session key", () => {
expect(extractDiscordChannelId("agent:main:discord:group:222333444")).toBe("222333444");
});
it("returns null for non-discord session key", () => {
expect(extractDiscordChannelId("agent:main:telegram:channel:123456789")).toBeNull();
});
it("returns null for session key without channel segment", () => {
expect(extractDiscordChannelId("agent:main:discord:dm:123456789")).toBeNull();
});
it("returns null for null input", () => {
expect(extractDiscordChannelId(null)).toBeNull();
});
it("returns null for undefined input", () => {
expect(extractDiscordChannelId(undefined)).toBeNull();
});
it("returns null for empty string", () => {
expect(extractDiscordChannelId("")).toBeNull();
});
it("extracts from longer session keys", () => {
expect(extractDiscordChannelId("agent:my-agent:discord:channel:111222333:thread:444555")).toBe(
"111222333",
);
for (const testCase of cases) {
expect(extractDiscordChannelId(testCase.input), testCase.name).toBe(testCase.expected);
}
});
});
@@ -353,19 +361,29 @@ describe("DiscordExecApprovalHandler.shouldHandle", () => {
// ─── DiscordExecApprovalHandler.getApprovers ──────────────────────────────────
describe("DiscordExecApprovalHandler.getApprovers", () => {
it("returns configured approvers", () => {
const handler = createHandler({ enabled: true, approvers: ["111", "222"] });
expect(handler.getApprovers()).toEqual(["111", "222"]);
});
it("returns approvers for configured, empty, and undefined lists", () => {
const cases = [
{
name: "configured approvers",
config: { enabled: true, approvers: ["111", "222"] } as DiscordExecApprovalConfig,
expected: ["111", "222"],
},
{
name: "empty approvers",
config: { enabled: true, approvers: [] } as DiscordExecApprovalConfig,
expected: [],
},
{
name: "undefined approvers",
config: { enabled: true } as DiscordExecApprovalConfig,
expected: [],
},
] as const;
it("returns empty array when no approvers configured", () => {
const handler = createHandler({ enabled: true, approvers: [] });
expect(handler.getApprovers()).toEqual([]);
});
it("returns empty array when approvers is undefined", () => {
const handler = createHandler({ enabled: true } as DiscordExecApprovalConfig);
expect(handler.getApprovers()).toEqual([]);
for (const testCase of cases) {
const handler = createHandler(testCase.config);
expect(handler.getApprovers(), testCase.name).toEqual(testCase.expected);
}
});
});
@@ -530,44 +548,46 @@ describe("DiscordExecApprovalHandler target config", () => {
mockRestDelete.mockReset();
});
it("defaults target to dm when not specified", () => {
const config: DiscordExecApprovalConfig = {
enabled: true,
approvers: ["123"],
};
// target should be undefined, handler defaults to "dm"
expect(config.target).toBeUndefined();
it("accepts all target modes and defaults to dm when target is omitted", () => {
const cases = [
{
name: "default target",
config: { enabled: true, approvers: ["123"] } as DiscordExecApprovalConfig,
expectedTarget: undefined,
},
{
name: "channel target",
config: {
enabled: true,
approvers: ["123"],
target: "channel",
} as DiscordExecApprovalConfig,
},
{
name: "both target",
config: {
enabled: true,
approvers: ["123"],
target: "both",
} as DiscordExecApprovalConfig,
},
{
name: "dm target",
config: {
enabled: true,
approvers: ["123"],
target: "dm",
} as DiscordExecApprovalConfig,
},
] as const;
const handler = createHandler(config);
// Handler should still handle requests (no crash on missing target)
expect(handler.shouldHandle(createRequest())).toBe(true);
});
it("accepts target=channel in config", () => {
const handler = createHandler({
enabled: true,
approvers: ["123"],
target: "channel",
});
expect(handler.shouldHandle(createRequest())).toBe(true);
});
it("accepts target=both in config", () => {
const handler = createHandler({
enabled: true,
approvers: ["123"],
target: "both",
});
expect(handler.shouldHandle(createRequest())).toBe(true);
});
it("accepts target=dm in config", () => {
const handler = createHandler({
enabled: true,
approvers: ["123"],
target: "dm",
});
expect(handler.shouldHandle(createRequest())).toBe(true);
for (const testCase of cases) {
if ("expectedTarget" in testCase) {
expect(testCase.config.target, testCase.name).toBe(testCase.expectedTarget);
}
const handler = createHandler(testCase.config);
expect(handler.shouldHandle(createRequest()), testCase.name).toBe(true);
}
});
});

View File

@@ -631,105 +631,133 @@ describe("resolveDiscordPresenceUpdate", () => {
});
describe("resolveDiscordAutoThreadContext", () => {
it("returns null when no createdThreadId", () => {
expect(
resolveDiscordAutoThreadContext({
it("returns null without a created thread and re-keys context when present", () => {
const cases = [
{
name: "no created thread",
createdThreadId: undefined,
expectedNull: true,
},
{
name: "created thread",
createdThreadId: "thread",
expectedNull: false,
},
] as const;
for (const testCase of cases) {
const context = resolveDiscordAutoThreadContext({
agentId: "agent",
channel: "discord",
messageChannelId: "parent",
createdThreadId: undefined,
}),
).toBeNull();
});
createdThreadId: testCase.createdThreadId,
});
it("re-keys session context to the created thread", () => {
const context = resolveDiscordAutoThreadContext({
agentId: "agent",
channel: "discord",
messageChannelId: "parent",
createdThreadId: "thread",
});
expect(context).not.toBeNull();
expect(context?.To).toBe("channel:thread");
expect(context?.From).toBe("discord:channel:thread");
expect(context?.OriginatingTo).toBe("channel:thread");
expect(context?.SessionKey).toBe(
buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "thread" },
}),
);
expect(context?.ParentSessionKey).toBe(
buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "parent" },
}),
);
if (testCase.expectedNull) {
expect(context, testCase.name).toBeNull();
continue;
}
expect(context, testCase.name).not.toBeNull();
expect(context?.To, testCase.name).toBe("channel:thread");
expect(context?.From, testCase.name).toBe("discord:channel:thread");
expect(context?.OriginatingTo, testCase.name).toBe("channel:thread");
expect(context?.SessionKey, testCase.name).toBe(
buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "thread" },
}),
);
expect(context?.ParentSessionKey, testCase.name).toBe(
buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "parent" },
}),
);
}
});
});
describe("resolveDiscordReplyDeliveryPlan", () => {
it("uses reply references when posting to the original target", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:parent",
replyToMode: "all",
messageId: "m1",
threadChannel: null,
createdThreadId: null,
});
expect(plan.deliverTarget).toBe("channel:parent");
expect(plan.replyTarget).toBe("channel:parent");
expect(plan.replyReference.use()).toBe("m1");
});
it("applies delivery targets and reply reference behavior across thread modes", () => {
const cases = [
{
name: "original target with reply references",
input: {
replyTarget: "channel:parent" as const,
replyToMode: "all" as const,
messageId: "m1",
threadChannel: null,
createdThreadId: null,
},
expectedDeliverTarget: "channel:parent",
expectedReplyTarget: "channel:parent",
expectedReplyReferenceCalls: ["m1"],
},
{
name: "created thread disables reply references",
input: {
replyTarget: "channel:parent" as const,
replyToMode: "all" as const,
messageId: "m1",
threadChannel: null,
createdThreadId: "thread",
},
expectedDeliverTarget: "channel:thread",
expectedReplyTarget: "channel:thread",
expectedReplyReferenceCalls: [undefined],
},
{
name: "thread + off mode",
input: {
replyTarget: "channel:thread" as const,
replyToMode: "off" as const,
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
},
expectedDeliverTarget: "channel:thread",
expectedReplyTarget: "channel:thread",
expectedReplyReferenceCalls: [undefined],
},
{
name: "thread + all mode",
input: {
replyTarget: "channel:thread" as const,
replyToMode: "all" as const,
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
},
expectedDeliverTarget: "channel:thread",
expectedReplyTarget: "channel:thread",
expectedReplyReferenceCalls: ["m1", "m1"],
},
{
name: "thread + first mode",
input: {
replyTarget: "channel:thread" as const,
replyToMode: "first" as const,
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
},
expectedDeliverTarget: "channel:thread",
expectedReplyTarget: "channel:thread",
expectedReplyReferenceCalls: ["m1", undefined],
},
] as const;
it("disables reply references when autoThread creates a new thread", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:parent",
replyToMode: "all",
messageId: "m1",
threadChannel: null,
createdThreadId: "thread",
});
expect(plan.deliverTarget).toBe("channel:thread");
expect(plan.replyTarget).toBe("channel:thread");
expect(plan.replyReference.use()).toBeUndefined();
});
it("respects replyToMode off even inside a thread", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:thread",
replyToMode: "off",
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
});
expect(plan.replyReference.use()).toBeUndefined();
});
it("uses existingId when inside a thread with replyToMode all", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:thread",
replyToMode: "all",
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
});
expect(plan.replyReference.use()).toBe("m1");
expect(plan.replyReference.use()).toBe("m1");
});
it("uses existingId only on first call with replyToMode first inside a thread", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:thread",
replyToMode: "first",
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
});
expect(plan.replyReference.use()).toBe("m1");
expect(plan.replyReference.use()).toBeUndefined();
for (const testCase of cases) {
const plan = resolveDiscordReplyDeliveryPlan(testCase.input);
expect(plan.deliverTarget, testCase.name).toBe(testCase.expectedDeliverTarget);
expect(plan.replyTarget, testCase.name).toBe(testCase.expectedReplyTarget);
for (const expected of testCase.expectedReplyReferenceCalls) {
expect(plan.replyReference.use(), testCase.name).toBe(expected);
}
}
});
});
@@ -751,34 +779,35 @@ describe("maybeCreateDiscordAutoThread", () => {
};
}
it("returns existing thread ID when creation fails due to race condition", async () => {
const client = {
rest: {
post: async () => {
throw new Error("A thread has already been created on this message");
},
get: async () => ({ thread: { id: "existing-thread" } }),
it("handles create-thread failures with and without an existing thread", async () => {
const cases = [
{
name: "race condition returns existing thread",
postError: "A thread has already been created on this message",
getResponse: { thread: { id: "existing-thread" } },
expected: "existing-thread",
},
} as unknown as Client;
const result = await maybeCreateDiscordAutoThread(createAutoThreadParams(client));
expect(result).toBe("existing-thread");
});
it("returns undefined when creation fails and no existing thread found", async () => {
const client = {
rest: {
post: async () => {
throw new Error("Some other error");
},
get: async () => ({ thread: null }),
{
name: "other error returns undefined",
postError: "Some other error",
getResponse: { thread: null },
expected: undefined,
},
} as unknown as Client;
] as const;
const result = await maybeCreateDiscordAutoThread(createAutoThreadParams(client));
for (const testCase of cases) {
const client = {
rest: {
post: async () => {
throw new Error(testCase.postError);
},
get: async () => testCase.getResponse,
},
} as unknown as Client;
expect(result).toBeUndefined();
const result = await maybeCreateDiscordAutoThread(createAutoThreadParams(client));
expect(result, testCase.name).toBe(testCase.expected);
}
});
});
@@ -809,38 +838,50 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
};
}
it("switches delivery + session context to the created thread", async () => {
const plan = await resolveDiscordAutoThreadReplyPlan(createAutoThreadPlanParams());
expect(plan.deliverTarget).toBe("channel:thread");
expect(plan.replyReference.use()).toBeUndefined();
expect(plan.autoThreadContext?.SessionKey).toBe(
buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "thread" },
}),
);
});
it("applies auto-thread reply planning across created, existing, and disabled modes", async () => {
const cases = [
{
name: "created thread",
params: undefined,
expectedDeliverTarget: "channel:thread",
expectedReplyReference: undefined,
expectedSessionKey: buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "thread" },
}),
},
{
name: "existing thread channel",
params: {
threadChannel: { id: "thread" },
},
expectedDeliverTarget: "channel:thread",
expectedReplyReference: "m1",
expectedSessionKey: null,
},
{
name: "autoThread disabled",
params: {
channelConfig: { autoThread: false } as unknown as DiscordChannelConfigResolved,
},
expectedDeliverTarget: "channel:parent",
expectedReplyReference: "m1",
expectedSessionKey: null,
},
] as const;
it("routes replies to an existing thread channel", async () => {
const plan = await resolveDiscordAutoThreadReplyPlan(
createAutoThreadPlanParams({
threadChannel: { id: "thread" },
}),
);
expect(plan.deliverTarget).toBe("channel:thread");
expect(plan.replyTarget).toBe("channel:thread");
expect(plan.replyReference.use()).toBe("m1");
expect(plan.autoThreadContext).toBeNull();
});
it("does nothing when autoThread is disabled", async () => {
const plan = await resolveDiscordAutoThreadReplyPlan(
createAutoThreadPlanParams({
channelConfig: { autoThread: false } as unknown as DiscordChannelConfigResolved,
}),
);
expect(plan.deliverTarget).toBe("channel:parent");
expect(plan.autoThreadContext).toBeNull();
for (const testCase of cases) {
const plan = await resolveDiscordAutoThreadReplyPlan(
createAutoThreadPlanParams(testCase.params),
);
expect(plan.deliverTarget, testCase.name).toBe(testCase.expectedDeliverTarget);
expect(plan.replyReference.use(), testCase.name).toBe(testCase.expectedReplyReference);
if (testCase.expectedSessionKey == null) {
expect(plan.autoThreadContext, testCase.name).toBeNull();
} else {
expect(plan.autoThreadContext?.SessionKey, testCase.name).toBe(testCase.expectedSessionKey);
}
}
});
});