fix: align draft/outbound typings and tests

This commit is contained in:
Peter Steinberger
2026-02-22 08:03:05 +00:00
parent 0ae7f962f9
commit 0c1a52307c
12 changed files with 49 additions and 33 deletions

View File

@@ -31,7 +31,7 @@ function requireGatewayTool(agentSessionKey?: string) {
function expectConfigMutationCall(params: { function expectConfigMutationCall(params: {
callGatewayTool: { callGatewayTool: {
mock: { mock: {
calls: Array<[string, unknown, Record<string, unknown>]>; calls: Array<readonly unknown[]>;
}; };
}; };
action: "config.apply" | "config.patch"; action: "config.apply" | "config.patch";

View File

@@ -1,4 +1,4 @@
import { createServer } from "node:http"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net"; import type { AddressInfo } from "node:net";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { import {
@@ -8,7 +8,7 @@ import {
import { getFreePort } from "./test-port.js"; import { getFreePort } from "./test-port.js";
async function withRelayServer( async function withRelayServer(
handler: Parameters<typeof createServer>[0], handler: (req: IncomingMessage, res: ServerResponse) => void,
run: (params: { port: number }) => Promise<void>, run: (params: { port: number }) => Promise<void>,
) { ) {
const port = await getFreePort(); const port = await getFreePort();

View File

@@ -51,7 +51,7 @@ describe("draft-stream-controls", () => {
it("clearFinalizableDraftMessage skips invalid message ids", async () => { it("clearFinalizableDraftMessage skips invalid message ids", async () => {
const deleteMessage = vi.fn(async () => {}); const deleteMessage = vi.fn(async () => {});
await clearFinalizableDraftMessage({ await clearFinalizableDraftMessage<unknown>({
stopForClear: async () => {}, stopForClear: async () => {},
readMessageId: () => 123, readMessageId: () => 123,
clearMessageId: () => {}, clearMessageId: () => {},

View File

@@ -19,7 +19,10 @@ type ClearFinalizableDraftMessageParams<T> = StopAndClearMessageIdParams<T> & {
warnPrefix: string; warnPrefix: string;
}; };
type FinalizableDraftLifecycleParams<T> = ClearFinalizableDraftMessageParams<T> & { type FinalizableDraftLifecycleParams<T> = Omit<
ClearFinalizableDraftMessageParams<T>,
"stopForClear"
> & {
throttleMs: number; throttleMs: number;
state: FinalizableDraftStreamState; state: FinalizableDraftStreamState;
sendOrEditStreamMessage: (text: string) => Promise<boolean>; sendOrEditStreamMessage: (text: string) => Promise<boolean>;

View File

@@ -43,10 +43,14 @@ export async function runChannelLogin(
runtime: RuntimeEnv = defaultRuntime, runtime: RuntimeEnv = defaultRuntime,
) { ) {
const { channelInput, plugin } = resolveChannelPluginForMode(opts, "login"); const { channelInput, plugin } = resolveChannelPluginForMode(opts, "login");
const login = plugin.auth?.login;
if (!login) {
throw new Error(`Channel ${channelInput} does not support login`);
}
// Auth-only flow: do not mutate channel config here. // Auth-only flow: do not mutate channel config here.
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));
const { cfg, accountId } = resolveAccountContext(plugin, opts); const { cfg, accountId } = resolveAccountContext(plugin, opts);
await plugin.auth!.login({ await login({
cfg, cfg,
accountId, accountId,
runtime, runtime,
@@ -59,11 +63,15 @@ export async function runChannelLogout(
opts: ChannelAuthOptions, opts: ChannelAuthOptions,
runtime: RuntimeEnv = defaultRuntime, runtime: RuntimeEnv = defaultRuntime,
) { ) {
const { plugin } = resolveChannelPluginForMode(opts, "logout"); const { channelInput, plugin } = resolveChannelPluginForMode(opts, "logout");
const logoutAccount = plugin.gateway?.logoutAccount;
if (!logoutAccount) {
throw new Error(`Channel ${channelInput} does not support logout`);
}
// Auth-only flow: resolve account + clear session state only. // Auth-only flow: resolve account + clear session state only.
const { cfg, accountId } = resolveAccountContext(plugin, opts); const { cfg, accountId } = resolveAccountContext(plugin, opts);
const account = plugin.config.resolveAccount(cfg, accountId); const account = plugin.config.resolveAccount(cfg, accountId);
await plugin.gateway!.logoutAccount({ await logoutAccount({
cfg, cfg,
accountId, accountId,
account, account,

View File

@@ -114,7 +114,9 @@ export function createDiscordDraftStream(params: {
streamMessageId = undefined; streamMessageId = undefined;
}, },
isValidMessageId: (value): value is string => typeof value === "string", isValidMessageId: (value): value is string => typeof value === "string",
deleteMessage: (messageId) => rest.delete(Routes.channelMessage(channelId, messageId)), deleteMessage: async (messageId) => {
await rest.delete(Routes.channelMessage(channelId, messageId));
},
warn: params.warn, warn: params.warn,
warnPrefix: "discord stream preview cleanup failed", warnPrefix: "discord stream preview cleanup failed",
}); });

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { packNpmSpecToArchive, withTempDir } from "./install-source-utils.js"; import { packNpmSpecToArchive, withTempDir } from "./install-source-utils.js";
import type { NpmIntegrityDriftPayload } from "./npm-integrity.js";
import { installFromNpmSpecArchive } from "./npm-pack-install.js"; import { installFromNpmSpecArchive } from "./npm-pack-install.js";
vi.mock("./install-source-utils.js", async (importOriginal) => { vi.mock("./install-source-utils.js", async (importOriginal) => {
@@ -37,12 +38,7 @@ describe("installFromNpmSpecArchive", () => {
const runInstall = async (overrides: { const runInstall = async (overrides: {
expectedIntegrity?: string; expectedIntegrity?: string;
onIntegrityDrift?: (payload: { onIntegrityDrift?: (payload: NpmIntegrityDriftPayload) => boolean | Promise<boolean>;
spec: string;
expectedIntegrity: string;
actualIntegrity: string;
resolvedSpec: string;
}) => boolean | Promise<boolean>;
warn?: (message: string) => void; warn?: (message: string) => void;
installFromArchive: (params: { installFromArchive: (params: {
archivePath: string; archivePath: string;

View File

@@ -4,8 +4,6 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ReplyPayload } from "../../auto-reply/types.js"; import type { ReplyPayload } from "../../auto-reply/types.js";
import type { OpenClawConfig } from "../../config/config.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 { typedCases } from "../../test-utils/typed-cases.js";
import { import {
ackDelivery, ackDelivery,

View File

@@ -160,7 +160,7 @@ export function resolveOutboundTarget(params: {
}; };
} }
const allowFrom = const allowFromRaw =
params.allowFrom ?? params.allowFrom ??
(params.cfg && plugin.config.resolveAllowFrom (params.cfg && plugin.config.resolveAllowFrom
? plugin.config.resolveAllowFrom({ ? plugin.config.resolveAllowFrom({
@@ -168,6 +168,7 @@ export function resolveOutboundTarget(params: {
accountId: params.accountId ?? undefined, accountId: params.accountId ?? undefined,
}) })
: undefined); : undefined);
const allowFrom = allowFromRaw?.map((entry) => String(entry));
// Fall back to per-channel defaultTo when no explicit target is provided. // Fall back to per-channel defaultTo when no explicit target is provided.
const effectiveTo = const effectiveTo =
@@ -360,12 +361,13 @@ export function resolveHeartbeatSenderContext(params: {
const accountId = const accountId =
params.delivery.accountId ?? params.delivery.accountId ??
(provider === params.delivery.lastChannel ? params.delivery.lastAccountId : undefined); (provider === params.delivery.lastChannel ? params.delivery.lastAccountId : undefined);
const allowFrom = provider const allowFromRaw = provider
? (getChannelPlugin(provider)?.config.resolveAllowFrom?.({ ? (getChannelPlugin(provider)?.config.resolveAllowFrom?.({
cfg: params.cfg, cfg: params.cfg,
accountId, accountId,
}) ?? []) }) ?? [])
: []; : [];
const allowFrom = allowFromRaw.map((entry) => String(entry));
const sender = resolveHeartbeatSenderId({ const sender = resolveHeartbeatSenderId({
allowFrom, allowFrom,

View File

@@ -1,5 +1,9 @@
import { vi } from "vitest"; import { vi } from "vitest";
import { buildTelegramMessageContext } from "./bot-message-context.js"; import {
buildTelegramMessageContext,
type BuildTelegramMessageContextParams,
type TelegramMediaRef,
} from "./bot-message-context.js";
export const baseTelegramMessageContextConfig = { export const baseTelegramMessageContextConfig = {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
@@ -9,15 +13,12 @@ export const baseTelegramMessageContextConfig = {
type BuildTelegramMessageContextForTestParams = { type BuildTelegramMessageContextForTestParams = {
message: Record<string, unknown>; message: Record<string, unknown>;
allMedia?: Array<Record<string, unknown>>; allMedia?: TelegramMediaRef[];
options?: Record<string, unknown>; options?: BuildTelegramMessageContextParams["options"];
cfg?: Record<string, unknown>; cfg?: Record<string, unknown>;
resolveGroupActivation?: () => boolean | undefined; resolveGroupActivation?: BuildTelegramMessageContextParams["resolveGroupActivation"];
resolveGroupRequireMention?: () => boolean; resolveGroupRequireMention?: BuildTelegramMessageContextParams["resolveGroupRequireMention"];
resolveTelegramGroupConfig?: () => { resolveTelegramGroupConfig?: BuildTelegramMessageContextParams["resolveTelegramGroupConfig"];
groupConfig?: { requireMention?: boolean };
topicConfig?: unknown;
};
}; };
export async function buildTelegramMessageContextForTest( export async function buildTelegramMessageContextForTest(

View File

@@ -153,7 +153,9 @@ export function createTelegramDraftStream(params: {
}, },
isValidMessageId: (value): value is number => isValidMessageId: (value): value is number =>
typeof value === "number" && Number.isFinite(value), typeof value === "number" && Number.isFinite(value),
deleteMessage: (messageId) => params.api.deleteMessage(chatId, messageId), deleteMessage: async (messageId) => {
await params.api.deleteMessage(chatId, messageId);
},
onDeleteSuccess: (messageId) => { onDeleteSuccess: (messageId) => {
params.log?.(`telegram stream preview deleted (chat=${chatId}, message=${messageId})`); params.log?.(`telegram stream preview deleted (chat=${chatId}, message=${messageId})`);
}, },

View File

@@ -1,19 +1,23 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { createCommandHandlers } from "./tui-command-handlers.js"; import { createCommandHandlers } from "./tui-command-handlers.js";
type LoadHistoryMock = ReturnType<typeof vi.fn> & (() => Promise<void>);
type SetActivityStatusMock = ReturnType<typeof vi.fn> & ((text: string) => void);
function createHarness(params?: { function createHarness(params?: {
sendChat?: ReturnType<typeof vi.fn>; sendChat?: ReturnType<typeof vi.fn>;
resetSession?: ReturnType<typeof vi.fn>; resetSession?: ReturnType<typeof vi.fn>;
loadHistory?: ReturnType<typeof vi.fn>; loadHistory?: LoadHistoryMock;
setActivityStatus?: ReturnType<typeof vi.fn>; setActivityStatus?: SetActivityStatusMock;
}) { }) {
const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" }); const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" });
const resetSession = params?.resetSession ?? vi.fn().mockResolvedValue({ ok: true }); const resetSession = params?.resetSession ?? vi.fn().mockResolvedValue({ ok: true });
const addUser = vi.fn(); const addUser = vi.fn();
const addSystem = vi.fn(); const addSystem = vi.fn();
const requestRender = vi.fn(); const requestRender = vi.fn();
const loadHistory = params?.loadHistory ?? vi.fn().mockResolvedValue(undefined); const loadHistory =
const setActivityStatus = params?.setActivityStatus ?? vi.fn(); params?.loadHistory ?? (vi.fn().mockResolvedValue(undefined) as LoadHistoryMock);
const setActivityStatus = params?.setActivityStatus ?? (vi.fn() as SetActivityStatusMock);
const { handleCommand } = createCommandHandlers({ const { handleCommand } = createCommandHandlers({
client: { sendChat, resetSession } as never, client: { sendChat, resetSession } as never,