refactor(test): stabilize case tables and readonly helper inputs

This commit is contained in:
Peter Steinberger
2026-02-22 00:00:53 +01:00
parent 03586e3d00
commit 8752203f59
11 changed files with 106 additions and 52 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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(

View File

@@ -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());