test: dedupe gateway browser discord and channel coverage

This commit is contained in:
Peter Steinberger
2026-02-22 17:11:42 +00:00
parent 34ea33f057
commit 296b19e413
29 changed files with 938 additions and 1041 deletions

View File

@@ -223,6 +223,12 @@ function buildExecApprovalPayload(container: DiscordUiContainer): MessagePayload
return { components };
}
function formatCommandPreview(commandText: string, maxChars: number): string {
const commandRaw =
commandText.length > maxChars ? `${commandText.slice(0, maxChars)}...` : commandText;
return commandRaw.replace(/`/g, "\u200b`");
}
function createExecApprovalRequestContainer(params: {
request: ExecApprovalRequest;
cfg: OpenClawConfig;
@@ -230,8 +236,7 @@ function createExecApprovalRequestContainer(params: {
actionRow?: Row<Button>;
}): ExecApprovalContainer {
const commandText = params.request.request.command;
const commandRaw = commandText.length > 1000 ? `${commandText.slice(0, 1000)}...` : commandText;
const commandPreview = commandRaw.replace(/`/g, "\u200b`");
const commandPreview = formatCommandPreview(commandText, 1000);
const expiresAtSeconds = Math.max(0, Math.floor(params.request.expiresAtMs / 1000));
return new ExecApprovalContainer({
@@ -255,8 +260,7 @@ function createResolvedContainer(params: {
accountId: string;
}): ExecApprovalContainer {
const commandText = params.request.request.command;
const commandRaw = commandText.length > 500 ? `${commandText.slice(0, 500)}...` : commandText;
const commandPreview = commandRaw.replace(/`/g, "\u200b`");
const commandPreview = formatCommandPreview(commandText, 500);
const decisionLabel =
params.decision === "allow-once"
@@ -289,8 +293,7 @@ function createExpiredContainer(params: {
accountId: string;
}): ExecApprovalContainer {
const commandText = params.request.request.command;
const commandRaw = commandText.length > 500 ? `${commandText.slice(0, 500)}...` : commandText;
const commandPreview = commandRaw.replace(/`/g, "\u200b`");
const commandPreview = formatCommandPreview(commandText, 500);
return new ExecApprovalContainer({
cfg: params.cfg,

View File

@@ -10,17 +10,21 @@ const sendMocks = vi.hoisted(() => ({
reactMessageDiscord: vi.fn(async () => {}),
removeReactionDiscord: vi.fn(async () => {}),
}));
const deliveryMocks = vi.hoisted(() => ({
editMessageDiscord: vi.fn(async () => ({})),
deliverDiscordReply: vi.fn(async () => {}),
createDiscordDraftStream: vi.fn(() => ({
function createMockDraftStream() {
return {
update: vi.fn<(text: string) => void>(() => {}),
flush: vi.fn(async () => {}),
messageId: vi.fn(() => "preview-1"),
clear: vi.fn(async () => {}),
stop: vi.fn(async () => {}),
forceNewMessage: vi.fn(() => {}),
})),
};
}
const deliveryMocks = vi.hoisted(() => ({
editMessageDiscord: vi.fn(async () => ({})),
deliverDiscordReply: vi.fn(async () => {}),
createDiscordDraftStream: vi.fn(() => createMockDraftStream()),
}));
const editMessageDiscord = deliveryMocks.editMessageDiscord;
const deliverDiscordReply = deliveryMocks.deliverDiscordReply;
@@ -373,17 +377,6 @@ describe("processDiscordMessage draft streaming", () => {
await processDiscordMessage(ctx as any);
}
function createMockDraftStream() {
return {
update: vi.fn<(text: string) => void>(() => {}),
flush: vi.fn(async () => {}),
messageId: vi.fn(() => "preview-1"),
clear: vi.fn(async () => {}),
stop: vi.fn(async () => {}),
forceNewMessage: vi.fn(() => {}),
};
}
async function createBlockModeContext() {
return await createBaseContext({
cfg: {
@@ -424,17 +417,7 @@ describe("processDiscordMessage draft streaming", () => {
});
it("falls back to standard send when final needs multiple chunks", async () => {
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.dispatcher.sendFinalReply({ text: "Hello\nWorld" });
return { queuedFinal: true, counts: { final: 1, tool: 0, block: 0 } };
});
const ctx = await createBaseContext({
discordConfig: { streamMode: "partial", maxLinesPerMessage: 1 },
});
// oxlint-disable-next-line typescript/no-explicit-any
await processDiscordMessage(ctx as any);
await runSingleChunkFinalScenario({ streamMode: "partial", maxLinesPerMessage: 1 });
expect(editMessageDiscord).not.toHaveBeenCalled();
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);

View File

@@ -0,0 +1,25 @@
import type { ModelsProviderData } from "../../auto-reply/reply/commands-models.js";
export function createModelsProviderData(
entries: Record<string, string[]>,
opts?: { defaultProviderOrder?: "insertion" | "sorted" },
): ModelsProviderData {
const byProvider = new Map<string, Set<string>>();
for (const [provider, models] of Object.entries(entries)) {
byProvider.set(provider, new Set(models));
}
const providers = Object.keys(entries).toSorted();
const insertionProvider = Object.keys(entries)[0];
const defaultProvider =
opts?.defaultProviderOrder === "sorted"
? (providers[0] ?? "openai")
: (insertionProvider ?? "openai");
return {
byProvider,
providers,
resolvedDefault: {
provider: defaultProvider,
model: entries[defaultProvider]?.[0] ?? "gpt-4o",
},
};
}

View File

@@ -1,7 +1,6 @@
import { serializePayload } from "@buape/carbon";
import { ComponentType } from "discord-api-types/v10";
import { describe, expect, it, vi } from "vitest";
import type { ModelsProviderData } from "../../auto-reply/reply/commands-models.js";
import * as modelsCommandModule from "../../auto-reply/reply/commands-models.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
@@ -20,21 +19,7 @@ import {
renderDiscordModelPickerRecentsView,
toDiscordModelPickerMessagePayload,
} from "./model-picker.js";
function createModelsProviderData(entries: Record<string, string[]>): ModelsProviderData {
const byProvider = new Map<string, Set<string>>();
for (const [provider, models] of Object.entries(entries)) {
byProvider.set(provider, new Set(models));
}
return {
byProvider,
providers: Object.keys(entries).toSorted(),
resolvedDefault: {
provider: Object.keys(entries)[0] ?? "openai",
model: entries[Object.keys(entries)[0]]?.[0] ?? "gpt-4o",
},
};
}
import { createModelsProviderData } from "./model-picker.test-utils.js";
type SerializedComponent = {
type: number;
@@ -55,6 +40,26 @@ function extractContainerRows(components?: SerializedComponent[]): SerializedCom
);
}
function renderModelsViewRows(
params: Parameters<typeof renderDiscordModelPickerModelsView>[0],
): SerializedComponent[] {
const rendered = renderDiscordModelPickerModelsView(params);
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
return extractContainerRows(payload.components);
}
function renderRecentsViewRows(
params: Parameters<typeof renderDiscordModelPickerRecentsView>[0],
): SerializedComponent[] {
const rendered = renderDiscordModelPickerRecentsView(params);
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
return extractContainerRows(payload.components);
}
describe("loadDiscordModelPickerData", () => {
it("reuses buildModelsProviderData as source of truth", async () => {
const expected = createModelsProviderData({ openai: ["gpt-4o"] });
@@ -467,7 +472,7 @@ describe("Discord model picker rendering", () => {
anthropic: ["claude-sonnet-4-5"],
});
const rendered = renderDiscordModelPickerModelsView({
const rows = renderModelsViewRows({
command: "model",
userId: "42",
data,
@@ -477,12 +482,6 @@ describe("Discord model picker rendering", () => {
currentModel: "openai/gpt-4o",
quickModels: ["openai/gpt-4o", "anthropic/claude-sonnet-4-5"],
});
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
const rows = extractContainerRows(payload.components);
const buttonRow = rows[2];
const buttons = buttonRow?.components ?? [];
expect(buttons).toHaveLength(4);
@@ -497,7 +496,7 @@ describe("Discord model picker rendering", () => {
openai: ["gpt-4.1", "gpt-4o"],
});
const rendered = renderDiscordModelPickerModelsView({
const rows = renderModelsViewRows({
command: "model",
userId: "42",
data,
@@ -506,12 +505,6 @@ describe("Discord model picker rendering", () => {
providerPage: 1,
currentModel: "openai/gpt-4o",
});
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
const rows = extractContainerRows(payload.components);
const buttonRow = rows[2];
const buttons = buttonRow?.components ?? [];
expect(buttons).toHaveLength(3);
@@ -532,19 +525,13 @@ describe("Discord model picker recents view", () => {
// Default is openai/gpt-4.1 (first key in entries).
// Neither quickModel matches, so no deduping — 1 default + 2 recents + 1 back = 4 rows.
const rendered = renderDiscordModelPickerRecentsView({
const rows = renderRecentsViewRows({
command: "model",
userId: "42",
data,
quickModels: ["openai/gpt-4o", "anthropic/claude-sonnet-4-5"],
currentModel: "openai/gpt-4o",
});
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
const rows = extractContainerRows(payload.components);
expect(rows).toHaveLength(4);
// First row: default model button (slot 1).
@@ -577,19 +564,13 @@ describe("Discord model picker recents view", () => {
openai: ["gpt-4o"],
});
const rendered = renderDiscordModelPickerRecentsView({
const rows = renderRecentsViewRows({
command: "model",
userId: "42",
data,
quickModels: ["openai/gpt-4o"],
currentModel: "openai/gpt-4o",
});
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
const rows = extractContainerRows(payload.components);
const defaultBtn = rows[0]?.components?.[0] as { label?: string };
expect(defaultBtn?.label).toContain("(default)");
});
@@ -600,19 +581,13 @@ describe("Discord model picker recents view", () => {
anthropic: ["claude-sonnet-4-5"],
});
// Default is openai/gpt-4o (first key). quickModels contains the default.
const rendered = renderDiscordModelPickerRecentsView({
const rows = renderRecentsViewRows({
command: "model",
userId: "42",
data,
quickModels: ["openai/gpt-4o", "anthropic/claude-sonnet-4-5"],
currentModel: "openai/gpt-4o",
});
const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
components?: SerializedComponent[];
};
const rows = extractContainerRows(payload.components);
// 1 default + 1 deduped recent + 1 back = 3 rows (openai/gpt-4o not shown twice)
expect(rows).toHaveLength(3);

View File

@@ -91,7 +91,7 @@ vi.mock("../../config/sessions.js", async (importOriginal) => {
describe("agent components", () => {
const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig;
const createDmButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
const createBaseDmInteraction = (overrides: Record<string, unknown> = {}) => {
const reply = vi.fn().mockResolvedValue(undefined);
const defer = vi.fn().mockResolvedValue(undefined);
const interaction = {
@@ -100,22 +100,31 @@ describe("agent components", () => {
defer,
reply,
...overrides,
} as unknown as ButtonInteraction;
};
return { interaction, defer, reply };
};
const createDmSelectInteraction = (overrides: Partial<StringSelectMenuInteraction> = {}) => {
const reply = vi.fn().mockResolvedValue(undefined);
const defer = vi.fn().mockResolvedValue(undefined);
const interaction = {
rawData: { channel_id: "dm-channel" },
user: { id: "123456789", username: "Alice", discriminator: "1234" },
values: ["alpha"],
const createDmButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
const { interaction, defer, reply } = createBaseDmInteraction(
overrides as Record<string, unknown>,
);
return {
interaction: interaction as unknown as ButtonInteraction,
defer,
reply,
...overrides,
} as unknown as StringSelectMenuInteraction;
return { interaction, defer, reply };
};
};
const createDmSelectInteraction = (overrides: Partial<StringSelectMenuInteraction> = {}) => {
const { interaction, defer, reply } = createBaseDmInteraction({
values: ["alpha"],
...(overrides as Record<string, unknown>),
});
return {
interaction: interaction as unknown as StringSelectMenuInteraction,
defer,
reply,
};
};
beforeEach(() => {

View File

@@ -12,28 +12,13 @@ import * as globalsModule from "../../globals.js";
import * as timeoutModule from "../../utils/with-timeout.js";
import * as modelPickerPreferencesModule from "./model-picker-preferences.js";
import * as modelPickerModule from "./model-picker.js";
import { createModelsProviderData as createBaseModelsProviderData } from "./model-picker.test-utils.js";
import {
createDiscordModelPickerFallbackButton,
createDiscordModelPickerFallbackSelect,
} from "./native-command.js";
import { createNoopThreadBindingManager, type ThreadBindingManager } from "./thread-bindings.js";
function createModelsProviderData(entries: Record<string, string[]>): ModelsProviderData {
const byProvider = new Map<string, Set<string>>();
for (const [provider, models] of Object.entries(entries)) {
byProvider.set(provider, new Set(models));
}
const providers = Object.keys(entries).toSorted();
return {
byProvider,
providers,
resolvedDefault: {
provider: providers[0] ?? "openai",
model: entries[providers[0] ?? "openai"]?.[0] ?? "gpt-4o",
},
};
}
type ModelPickerContext = Parameters<typeof createDiscordModelPickerFallbackButton>[0];
type PickerButton = ReturnType<typeof createDiscordModelPickerFallbackButton>;
type PickerSelect = ReturnType<typeof createDiscordModelPickerFallbackSelect>;
@@ -55,6 +40,10 @@ type MockInteraction = {
client: object;
};
function createModelsProviderData(entries: Record<string, string[]>): ModelsProviderData {
return createBaseModelsProviderData(entries, { defaultProviderOrder: "sorted" });
}
function createModelPickerContext(): ModelPickerContext {
const cfg = {
channels: {
@@ -152,6 +141,36 @@ function createModelsViewSubmitData(): PickerButtonData {
};
}
async function runSubmitButton(params: {
context: ModelPickerContext;
data: PickerButtonData;
userId?: string;
}) {
const button = createDiscordModelPickerFallbackButton(params.context);
const submitInteraction = createInteraction({ userId: params.userId ?? "owner" });
await button.run(submitInteraction as unknown as PickerButtonInteraction, params.data);
return submitInteraction;
}
function expectDispatchedModelSelection(params: {
dispatchSpy: { mock: { calls: Array<[unknown]> } };
model: string;
requireTargetSessionKey?: boolean;
}) {
const dispatchCall = params.dispatchSpy.mock.calls[0]?.[0] as {
ctx?: {
CommandBody?: string;
CommandArgs?: { values?: { model?: string } };
CommandTargetSessionKey?: string;
};
};
expect(dispatchCall.ctx?.CommandBody).toBe(`/model ${params.model}`);
expect(dispatchCall.ctx?.CommandArgs?.values?.model).toBe(params.model);
if (params.requireTargetSessionKey) {
expect(dispatchCall.ctx?.CommandTargetSessionKey).toBeDefined();
}
}
function createBoundThreadBindingManager(params: {
accountId: string;
threadId: string;
@@ -244,25 +263,18 @@ describe("Discord model picker interactions", () => {
expect(selectInteraction.update).toHaveBeenCalledTimes(1);
expect(dispatchSpy).not.toHaveBeenCalled();
const button = createDiscordModelPickerFallbackButton(context);
const submitInteraction = createInteraction({ userId: "owner" });
const submitData = createModelsViewSubmitData();
await button.run(submitInteraction as unknown as PickerButtonInteraction, submitData);
const submitInteraction = await runSubmitButton({
context,
data: createModelsViewSubmitData(),
});
expect(submitInteraction.update).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(1);
const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as {
ctx?: {
CommandBody?: string;
CommandArgs?: { values?: { model?: string } };
CommandTargetSessionKey?: string;
};
};
expect(dispatchCall.ctx?.CommandBody).toBe("/model openai/gpt-4o");
expect(dispatchCall.ctx?.CommandArgs?.values?.model).toBe("openai/gpt-4o");
expect(dispatchCall.ctx?.CommandTargetSessionKey).toBeDefined();
expectDispatchedModelSelection({
dispatchSpy,
model: "openai/gpt-4o",
requireTargetSessionKey: true,
});
});
it("shows timeout status and skips recents write when apply is still processing", async () => {
@@ -359,31 +371,22 @@ describe("Discord model picker interactions", () => {
.spyOn(dispatcherModule, "dispatchReplyWithDispatcher")
.mockResolvedValue({} as never);
const button = createDiscordModelPickerFallbackButton(context);
const submitInteraction = createInteraction({ userId: "owner" });
// rs=2 → first deduped recent (default is anthropic/claude-sonnet-4-5, so openai/gpt-4o remains)
const submitData: PickerButtonData = {
cmd: "model",
act: "submit",
view: "recents",
u: "owner",
pg: "1",
rs: "2",
};
await button.run(submitInteraction as unknown as PickerButtonInteraction, submitData);
// rs=2 -> first deduped recent (default is anthropic/claude-sonnet-4-5, so openai/gpt-4o remains)
const submitInteraction = await runSubmitButton({
context,
data: {
cmd: "model",
act: "submit",
view: "recents",
u: "owner",
pg: "1",
rs: "2",
},
});
expect(submitInteraction.update).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(1);
const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as {
ctx?: {
CommandBody?: string;
CommandArgs?: { values?: { model?: string } };
};
};
expect(dispatchCall.ctx?.CommandBody).toBe("/model openai/gpt-4o");
expect(dispatchCall.ctx?.CommandArgs?.values?.model).toBe("openai/gpt-4o");
expectDispatchedModelSelection({ dispatchSpy, model: "openai/gpt-4o" });
});
it("verifies model state against the bound thread session", async () => {

View File

@@ -70,6 +70,20 @@ describe("runDiscordGatewayLifecycle", () => {
};
};
function expectLifecycleCleanup(params: {
start: ReturnType<typeof vi.fn>;
stop: ReturnType<typeof vi.fn>;
threadStop: ReturnType<typeof vi.fn>;
waitCalls: number;
}) {
expect(params.start).toHaveBeenCalledTimes(1);
expect(params.stop).toHaveBeenCalledTimes(1);
expect(waitForDiscordGatewayStopMock).toHaveBeenCalledTimes(params.waitCalls);
expect(unregisterGatewayMock).toHaveBeenCalledWith("default");
expect(stopGatewayLoggingMock).toHaveBeenCalledTimes(1);
expect(params.threadStop).toHaveBeenCalledTimes(1);
}
it("cleans up thread bindings when exec approvals startup fails", async () => {
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
const { lifecycleParams, start, stop, threadStop } = createLifecycleHarness({
@@ -80,12 +94,7 @@ describe("runDiscordGatewayLifecycle", () => {
await expect(runDiscordGatewayLifecycle(lifecycleParams)).rejects.toThrow("startup failed");
expect(start).toHaveBeenCalledTimes(1);
expect(stop).toHaveBeenCalledTimes(1);
expect(waitForDiscordGatewayStopMock).not.toHaveBeenCalled();
expect(unregisterGatewayMock).toHaveBeenCalledWith("default");
expect(stopGatewayLoggingMock).toHaveBeenCalledTimes(1);
expect(threadStop).toHaveBeenCalledTimes(1);
expectLifecycleCleanup({ start, stop, threadStop, waitCalls: 0 });
});
it("cleans up when gateway wait fails after startup", async () => {
@@ -97,12 +106,7 @@ describe("runDiscordGatewayLifecycle", () => {
"gateway wait failed",
);
expect(start).toHaveBeenCalledTimes(1);
expect(stop).toHaveBeenCalledTimes(1);
expect(waitForDiscordGatewayStopMock).toHaveBeenCalledTimes(1);
expect(unregisterGatewayMock).toHaveBeenCalledWith("default");
expect(stopGatewayLoggingMock).toHaveBeenCalledTimes(1);
expect(threadStop).toHaveBeenCalledTimes(1);
expectLifecycleCleanup({ start, stop, threadStop, waitCalls: 1 });
});
it("cleans up after successful gateway wait", async () => {
@@ -111,11 +115,6 @@ describe("runDiscordGatewayLifecycle", () => {
await expect(runDiscordGatewayLifecycle(lifecycleParams)).resolves.toBeUndefined();
expect(start).toHaveBeenCalledTimes(1);
expect(stop).toHaveBeenCalledTimes(1);
expect(waitForDiscordGatewayStopMock).toHaveBeenCalledTimes(1);
expect(unregisterGatewayMock).toHaveBeenCalledWith("default");
expect(stopGatewayLoggingMock).toHaveBeenCalledTimes(1);
expect(threadStop).toHaveBeenCalledTimes(1);
expectLifecycleCleanup({ start, stop, threadStop, waitCalls: 1 });
});
});

View File

@@ -70,6 +70,7 @@ import { resolveDiscordAllowlistConfig } from "./provider.allowlist.js";
import { runDiscordGatewayLifecycle } from "./provider.lifecycle.js";
import { resolveDiscordRestFetch } from "./rest-fetch.js";
import { createNoopThreadBindingManager, createThreadBindingManager } from "./thread-bindings.js";
import { formatThreadBindingTtlLabel } from "./thread-bindings.messages.js";
export type MonitorDiscordOpts = {
token?: string;
@@ -143,17 +144,8 @@ function resolveThreadBindingsEnabled(params: {
}
function formatThreadBindingSessionTtlLabel(ttlMs: number): string {
if (ttlMs <= 0) {
return "off";
}
if (ttlMs < 60_000) {
return "<1m";
}
const totalMinutes = Math.floor(ttlMs / 60_000);
if (totalMinutes % 60 === 0) {
return `${Math.floor(totalMinutes / 60)}h`;
}
return `${totalMinutes}m`;
const label = formatThreadBindingTtlLabel(ttlMs);
return label === "disabled" ? "off" : label;
}
function dedupeSkillCommandsForDiscord(

View File

@@ -22,6 +22,24 @@ import {
} from "./thread-bindings.state.js";
import type { ThreadBindingRecord, ThreadBindingTargetKind } from "./thread-bindings.types.js";
function resolveBindingIdsForTargetSession(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}) {
ensureBindingsLoaded();
const targetSessionKey = params.targetSessionKey.trim();
if (!targetSessionKey) {
return [];
}
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
return resolveBindingIdsForSession({
targetSessionKey,
accountId,
targetKind: params.targetKind,
});
}
export function listThreadBindingsForAccount(accountId?: string): ThreadBindingRecord[] {
const manager = getThreadBindingManager(accountId);
if (!manager) {
@@ -35,17 +53,7 @@ export function listThreadBindingsBySessionKey(params: {
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}): ThreadBindingRecord[] {
ensureBindingsLoaded();
const targetSessionKey = params.targetSessionKey.trim();
if (!targetSessionKey) {
return [];
}
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
const ids = resolveBindingIdsForSession({
targetSessionKey,
accountId,
targetKind: params.targetKind,
});
const ids = resolveBindingIdsForTargetSession(params);
return ids
.map((bindingKey) => BINDINGS_BY_THREAD_ID.get(bindingKey))
.filter((entry): entry is ThreadBindingRecord => Boolean(entry));
@@ -136,17 +144,7 @@ export function unbindThreadBindingsBySessionKey(params: {
sendFarewell?: boolean;
farewellText?: string;
}): ThreadBindingRecord[] {
ensureBindingsLoaded();
const targetSessionKey = params.targetSessionKey.trim();
if (!targetSessionKey) {
return [];
}
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
const ids = resolveBindingIdsForSession({
targetSessionKey,
accountId,
targetKind: params.targetKind,
});
const ids = resolveBindingIdsForTargetSession(params);
if (ids.length === 0) {
return [];
}
@@ -188,16 +186,7 @@ export function setThreadBindingTtlBySessionKey(params: {
accountId?: string;
ttlMs: number;
}): ThreadBindingRecord[] {
ensureBindingsLoaded();
const targetSessionKey = params.targetSessionKey.trim();
if (!targetSessionKey) {
return [];
}
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
const ids = resolveBindingIdsForSession({
targetSessionKey,
accountId,
});
const ids = resolveBindingIdsForTargetSession(params);
if (ids.length === 0) {
return [];
}