refactor(test): stabilize case tables and readonly helper inputs
This commit is contained in:
@@ -408,15 +408,15 @@ describe("redactConfigSnapshot", () => {
|
||||
custom2: [{ mySecret: "this-is-a-custom-secret-value" }],
|
||||
}),
|
||||
assert: ({ redacted, restored }) => {
|
||||
const cfg = redacted as Record<string, Record<string, unknown>>;
|
||||
const cfgCustom2 = cfg.custom2 as unknown as unknown[];
|
||||
const cfg = redacted;
|
||||
const cfgCustom2 = Array.isArray(cfg.custom2) ? cfg.custom2 : [];
|
||||
expect(cfgCustom2.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
((cfg.custom1 as Record<string, unknown>).anykey as Record<string, unknown>).mySecret,
|
||||
).toBe(REDACTED_SENTINEL);
|
||||
expect((cfgCustom2[0] as Record<string, unknown>).mySecret).toBe(REDACTED_SENTINEL);
|
||||
const out = restored as Record<string, Record<string, unknown>>;
|
||||
const outCustom2 = out.custom2 as unknown as unknown[];
|
||||
const out = restored;
|
||||
const outCustom2 = Array.isArray(out.custom2) ? out.custom2 : [];
|
||||
expect(outCustom2.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
((out.custom1 as Record<string, unknown>).anykey as Record<string, unknown>).mySecret,
|
||||
@@ -437,15 +437,15 @@ describe("redactConfigSnapshot", () => {
|
||||
custom2: [{ mySecret: "this-is-a-custom-secret-value" }],
|
||||
}),
|
||||
assert: ({ redacted, restored }) => {
|
||||
const cfg = redacted as Record<string, Record<string, unknown>>;
|
||||
const cfgCustom2 = cfg.custom2 as unknown as unknown[];
|
||||
const cfg = redacted;
|
||||
const cfgCustom2 = Array.isArray(cfg.custom2) ? cfg.custom2 : [];
|
||||
expect(cfgCustom2.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
((cfg.custom1 as Record<string, unknown>).anykey as Record<string, unknown>).mySecret,
|
||||
).toBe(REDACTED_SENTINEL);
|
||||
expect((cfgCustom2[0] as Record<string, unknown>).mySecret).toBe(REDACTED_SENTINEL);
|
||||
const out = restored as Record<string, Record<string, unknown>>;
|
||||
const outCustom2 = out.custom2 as unknown as unknown[];
|
||||
const out = restored;
|
||||
const outCustom2 = Array.isArray(out.custom2) ? out.custom2 : [];
|
||||
expect(outCustom2.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
((out.custom1 as Record<string, unknown>).anykey as Record<string, unknown>).mySecret,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChannelType, type Guild } from "@buape/carbon";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { typedCases } from "../test-utils/typed-cases.js";
|
||||
import {
|
||||
allowListMatches,
|
||||
buildDiscordMediaPayload,
|
||||
@@ -637,7 +638,11 @@ describe("discord autoThread name sanitization", () => {
|
||||
|
||||
describe("discord reaction notification gating", () => {
|
||||
it("applies mode-specific reaction notification rules", () => {
|
||||
const cases = [
|
||||
const cases = typedCases<{
|
||||
name: string;
|
||||
input: Parameters<typeof shouldEmitDiscordReactionNotification>[0];
|
||||
expected: boolean;
|
||||
}>([
|
||||
{
|
||||
name: "unset defaults to own (author is bot)",
|
||||
input: {
|
||||
@@ -721,7 +726,7 @@ describe("discord reaction notification gating", () => {
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
] as const;
|
||||
]);
|
||||
|
||||
for (const testCase of cases) {
|
||||
expect(
|
||||
@@ -963,7 +968,18 @@ describe("discord reaction notification modes", () => {
|
||||
const guild = fakeGuild(guildId, "Mode Guild");
|
||||
|
||||
it("applies message-fetch behavior across notification modes and channel types", async () => {
|
||||
const cases = [
|
||||
const cases = typedCases<{
|
||||
name: string;
|
||||
reactionNotifications: "off" | "all" | "allowlist" | "own";
|
||||
users: string[] | undefined;
|
||||
userId: string | undefined;
|
||||
channelType: ChannelType;
|
||||
channelId: string | undefined;
|
||||
parentId: string | undefined;
|
||||
messageAuthorId: string;
|
||||
expectedMessageFetchCalls: number;
|
||||
expectedEnqueueCalls: number;
|
||||
}>([
|
||||
{
|
||||
name: "off mode",
|
||||
reactionNotifications: "off" as const,
|
||||
@@ -1024,7 +1040,7 @@ describe("discord reaction notification modes", () => {
|
||||
expectedMessageFetchCalls: 0,
|
||||
expectedEnqueueCalls: 1,
|
||||
},
|
||||
] as const;
|
||||
]);
|
||||
|
||||
for (const testCase of cases) {
|
||||
enqueueSystemEventSpy.mockClear();
|
||||
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { buildAgentPeerSessionKey } from "../routing/session-key.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { typedCases } from "../test-utils/typed-cases.js";
|
||||
import {
|
||||
isHeartbeatEnabledForAgent,
|
||||
type HeartbeatDeps,
|
||||
isHeartbeatEnabledForAgent,
|
||||
resolveHeartbeatIntervalMs,
|
||||
resolveHeartbeatPrompt,
|
||||
runHeartbeatOnce,
|
||||
@@ -680,7 +681,15 @@ describe("runHeartbeatOnce", () => {
|
||||
it("resolves configured and forced session key overrides", async () => {
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
const cases = [
|
||||
const cases = typedCases<{
|
||||
name: string;
|
||||
caseDir: string;
|
||||
peerKind: "group" | "direct";
|
||||
peerId: string;
|
||||
message: string;
|
||||
applyOverride: (params: { cfg: OpenClawConfig; sessionKey: string }) => void;
|
||||
runOptions: (params: { sessionKey: string }) => { sessionKey?: string };
|
||||
}>([
|
||||
{
|
||||
name: "heartbeat.session",
|
||||
caseDir: "hb-explicit-session",
|
||||
@@ -705,7 +714,7 @@ describe("runHeartbeatOnce", () => {
|
||||
applyOverride: () => {},
|
||||
runOptions: ({ sessionKey }: { sessionKey: string }) => ({ sessionKey }),
|
||||
},
|
||||
] as const;
|
||||
]);
|
||||
|
||||
for (const testCase of cases) {
|
||||
const tmpDir = await createCaseDir(testCase.caseDir);
|
||||
@@ -835,12 +844,12 @@ describe("runHeartbeatOnce", () => {
|
||||
it("handles reasoning payload delivery variants", async () => {
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
const cases: Array<{
|
||||
const cases = typedCases<{
|
||||
name: string;
|
||||
caseDir: string;
|
||||
replies: Array<{ text: string }>;
|
||||
expectedTexts: string[];
|
||||
}> = [
|
||||
}>([
|
||||
{
|
||||
name: "reasoning + final payload",
|
||||
caseDir: "hb-reasoning",
|
||||
@@ -853,7 +862,7 @@ describe("runHeartbeatOnce", () => {
|
||||
replies: [{ text: "Reasoning:\n_Because it helps_" }, { text: "HEARTBEAT_OK" }],
|
||||
expectedTexts: ["Reasoning:\n_Because it helps_"],
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
for (const testCase of cases) {
|
||||
const tmpDir = await createCaseDir(testCase.caseDir);
|
||||
|
||||
@@ -9,7 +9,7 @@ export type OutboundResultEnvelope = {
|
||||
};
|
||||
|
||||
type BuildEnvelopeParams = {
|
||||
payloads?: ReplyPayload[] | OutboundPayloadJson[];
|
||||
payloads?: readonly ReplyPayload[] | readonly OutboundPayloadJson[];
|
||||
meta?: unknown;
|
||||
delivery?: OutboundDeliveryJson;
|
||||
flattenDelivery?: boolean;
|
||||
@@ -29,8 +29,8 @@ export function buildOutboundResultEnvelope(
|
||||
: params.payloads.length === 0
|
||||
? []
|
||||
: isOutboundPayloadJson(params.payloads[0])
|
||||
? (params.payloads as OutboundPayloadJson[])
|
||||
: normalizeOutboundPayloadsForJson(params.payloads as ReplyPayload[]);
|
||||
? [...(params.payloads as readonly OutboundPayloadJson[])]
|
||||
: normalizeOutboundPayloadsForJson(params.payloads as readonly ReplyPayload[]);
|
||||
|
||||
if (params.flattenDelivery !== false && params.delivery && !params.meta && !hasPayloads) {
|
||||
return params.delivery;
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { typedCases } from "../../test-utils/typed-cases.js";
|
||||
import {
|
||||
ackDelivery,
|
||||
computeBackoffMs,
|
||||
@@ -447,7 +448,11 @@ describe("buildOutboundResultEnvelope", () => {
|
||||
mediaUrl: null,
|
||||
channelId: "C1",
|
||||
};
|
||||
const cases = [
|
||||
const cases = typedCases<{
|
||||
name: string;
|
||||
input: Parameters<typeof buildOutboundResultEnvelope>[0];
|
||||
expected: unknown;
|
||||
}>([
|
||||
{
|
||||
name: "flatten delivery by default",
|
||||
input: { delivery: whatsappDelivery },
|
||||
@@ -478,7 +483,7 @@ describe("buildOutboundResultEnvelope", () => {
|
||||
input: { delivery: discordDelivery, flattenDelivery: false },
|
||||
expected: { delivery: discordDelivery },
|
||||
},
|
||||
];
|
||||
]);
|
||||
for (const testCase of cases) {
|
||||
const input: Parameters<typeof buildOutboundResultEnvelope>[0] =
|
||||
"payloads" in testCase.input
|
||||
@@ -814,7 +819,10 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
|
||||
describe("normalizeOutboundPayloadsForJson", () => {
|
||||
it("normalizes payloads for JSON output", () => {
|
||||
const cases = [
|
||||
const cases = typedCases<{
|
||||
input: Parameters<typeof normalizeOutboundPayloadsForJson>[0];
|
||||
expected: ReturnType<typeof normalizeOutboundPayloadsForJson>;
|
||||
}>([
|
||||
{
|
||||
input: [
|
||||
{ text: "hi" },
|
||||
@@ -852,7 +860,7 @@ describe("normalizeOutboundPayloadsForJson", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
for (const testCase of cases) {
|
||||
const input: ReplyPayload[] = testCase.input.map((payload) =>
|
||||
@@ -878,7 +886,11 @@ describe("normalizeOutboundPayloads", () => {
|
||||
|
||||
describe("formatOutboundPayloadLog", () => {
|
||||
it("formats text+media and media-only logs", () => {
|
||||
const cases = [
|
||||
const cases = typedCases<{
|
||||
name: string;
|
||||
input: Parameters<typeof formatOutboundPayloadLog>[0];
|
||||
expected: string;
|
||||
}>([
|
||||
{
|
||||
name: "text with media lines",
|
||||
input: {
|
||||
@@ -895,7 +907,7 @@ describe("formatOutboundPayloadLog", () => {
|
||||
},
|
||||
expected: "MEDIA:https://x.test/a.png",
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
for (const testCase of cases) {
|
||||
expect(
|
||||
|
||||
@@ -15,7 +15,7 @@ export type OutboundPayloadJson = {
|
||||
channelData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function mergeMediaUrls(...lists: Array<Array<string | undefined> | undefined>): string[] {
|
||||
function mergeMediaUrls(...lists: Array<ReadonlyArray<string | undefined> | undefined>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const merged: string[] = [];
|
||||
for (const list of lists) {
|
||||
@@ -37,7 +37,9 @@ function mergeMediaUrls(...lists: Array<Array<string | undefined> | undefined>):
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function normalizeReplyPayloadsForDelivery(payloads: ReplyPayload[]): ReplyPayload[] {
|
||||
export function normalizeReplyPayloadsForDelivery(
|
||||
payloads: readonly ReplyPayload[],
|
||||
): ReplyPayload[] {
|
||||
return payloads.flatMap((payload) => {
|
||||
const parsed = parseReplyDirectives(payload.text ?? "");
|
||||
const explicitMediaUrls = payload.mediaUrls ?? parsed.mediaUrls;
|
||||
@@ -68,7 +70,9 @@ export function normalizeReplyPayloadsForDelivery(payloads: ReplyPayload[]): Rep
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeOutboundPayloads(payloads: ReplyPayload[]): NormalizedOutboundPayload[] {
|
||||
export function normalizeOutboundPayloads(
|
||||
payloads: readonly ReplyPayload[],
|
||||
): NormalizedOutboundPayload[] {
|
||||
return normalizeReplyPayloadsForDelivery(payloads)
|
||||
.map((payload) => {
|
||||
const channelData = payload.channelData;
|
||||
@@ -89,7 +93,9 @@ export function normalizeOutboundPayloads(payloads: ReplyPayload[]): NormalizedO
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeOutboundPayloadsForJson(payloads: ReplyPayload[]): OutboundPayloadJson[] {
|
||||
export function normalizeOutboundPayloadsForJson(
|
||||
payloads: readonly ReplyPayload[],
|
||||
): OutboundPayloadJson[] {
|
||||
return normalizeReplyPayloadsForDelivery(payloads).map((payload) => ({
|
||||
text: payload.text ?? "",
|
||||
mediaUrl: payload.mediaUrl ?? null,
|
||||
@@ -98,7 +104,11 @@ export function normalizeOutboundPayloadsForJson(payloads: ReplyPayload[]): Outb
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatOutboundPayloadLog(payload: NormalizedOutboundPayload): string {
|
||||
export function formatOutboundPayloadLog(
|
||||
payload: Pick<NormalizedOutboundPayload, "text" | "channelData"> & {
|
||||
mediaUrls: readonly string[];
|
||||
},
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
if (payload.text) {
|
||||
lines.push(payload.text.trimEnd());
|
||||
|
||||
@@ -6,4 +6,4 @@ export type TelegramInlineButton = {
|
||||
style?: TelegramButtonStyle;
|
||||
};
|
||||
|
||||
export type TelegramInlineButtons = TelegramInlineButton[][];
|
||||
export type TelegramInlineButtons = ReadonlyArray<ReadonlyArray<TelegramInlineButton>>;
|
||||
|
||||
@@ -237,12 +237,12 @@ describe("edge cases", () => {
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
const result = markdownToTelegramHtml(testCase.input);
|
||||
if ("contains" in testCase) {
|
||||
if ("contains" in testCase && testCase.contains) {
|
||||
for (const expected of testCase.contains) {
|
||||
expect(result, testCase.name).toContain(expected);
|
||||
}
|
||||
}
|
||||
if ("notContains" in testCase) {
|
||||
if ("notContains" in testCase && testCase.notContains) {
|
||||
for (const unexpected of testCase.notContains) {
|
||||
expect(result, testCase.name).not.toContain(unexpected);
|
||||
}
|
||||
@@ -301,7 +301,7 @@ describe("edge cases", () => {
|
||||
if ("expectedExact" in testCase) {
|
||||
expect(result, testCase.name).toBe(testCase.expectedExact);
|
||||
}
|
||||
if ("contains" in testCase) {
|
||||
if ("contains" in testCase && testCase.contains) {
|
||||
for (const expected of testCase.contains) {
|
||||
expect(result, testCase.name).toContain(expected);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export type ProviderInfo = {
|
||||
|
||||
export type ModelsKeyboardParams = {
|
||||
provider: string;
|
||||
models: string[];
|
||||
models: readonly string[];
|
||||
currentModel?: string;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
|
||||
@@ -596,22 +596,22 @@ describe("sendMessageTelegram", () => {
|
||||
fileName: "video.mp4",
|
||||
});
|
||||
|
||||
const opts: Parameters<typeof sendMessageTelegram>[2] = {
|
||||
const sendOptions: NonNullable<Parameters<typeof sendMessageTelegram>[2]> = {
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/video.mp4",
|
||||
asVideoNote: true,
|
||||
...("replyToMessageId" in testCase.options
|
||||
? { replyToMessageId: testCase.options.replyToMessageId }
|
||||
: {}),
|
||||
...(Array.isArray(testCase.options.buttons)
|
||||
? {
|
||||
buttons: testCase.options.buttons.map((row) => row.map((button) => ({ ...button }))),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
await sendMessageTelegram(chatId, testCase.text, opts);
|
||||
if (
|
||||
"replyToMessageId" in testCase.options &&
|
||||
testCase.options.replyToMessageId !== undefined
|
||||
) {
|
||||
sendOptions.replyToMessageId = testCase.options.replyToMessageId;
|
||||
}
|
||||
if ("buttons" in testCase.options && testCase.options.buttons) {
|
||||
sendOptions.buttons = testCase.options.buttons;
|
||||
}
|
||||
await sendMessageTelegram(chatId, testCase.text, sendOptions);
|
||||
|
||||
expect(sendVideoNote).toHaveBeenCalledWith(
|
||||
chatId,
|
||||
@@ -790,8 +790,12 @@ describe("sendMessageTelegram", () => {
|
||||
api,
|
||||
mediaUrl: testCase.mediaUrl,
|
||||
...("asVoice" in testCase && testCase.asVoice ? { asVoice: true } : {}),
|
||||
...("messageThreadId" in testCase ? { messageThreadId: testCase.messageThreadId } : {}),
|
||||
...("replyToMessageId" in testCase ? { replyToMessageId: testCase.replyToMessageId } : {}),
|
||||
...("messageThreadId" in testCase && testCase.messageThreadId !== undefined
|
||||
? { messageThreadId: testCase.messageThreadId }
|
||||
: {}),
|
||||
...("replyToMessageId" in testCase && testCase.replyToMessageId !== undefined
|
||||
? { replyToMessageId: testCase.replyToMessageId }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const called = testCase.expectedMethod === "sendVoice" ? sendVoice : sendAudio;
|
||||
@@ -1321,13 +1325,13 @@ describe("editMessageTelegram", () => {
|
||||
if ("firstExpectNoReplyMarkup" in testCase && testCase.firstExpectNoReplyMarkup) {
|
||||
expect(firstParams, testCase.name).not.toHaveProperty("reply_markup");
|
||||
}
|
||||
if ("firstExpectReplyMarkup" in testCase) {
|
||||
if ("firstExpectReplyMarkup" in testCase && testCase.firstExpectReplyMarkup) {
|
||||
expect(firstParams, testCase.name).toEqual(
|
||||
expect.objectContaining({ reply_markup: testCase.firstExpectReplyMarkup }),
|
||||
);
|
||||
}
|
||||
|
||||
if ("secondExpectReplyMarkup" in testCase) {
|
||||
if ("secondExpectReplyMarkup" in testCase && testCase.secondExpectReplyMarkup) {
|
||||
const secondParams = (botApi.editMessageText.mock.calls[1] ?? [])[3] as Record<
|
||||
string,
|
||||
unknown
|
||||
|
||||
3
src/test-utils/typed-cases.ts
Normal file
3
src/test-utils/typed-cases.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function typedCases<T>(cases: T[]): T[] {
|
||||
return cases;
|
||||
}
|
||||
Reference in New Issue
Block a user