Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,607 @@
/**
* Tests for outbound.ts module
*
* Tests cover:
* - resolveTarget with various modes (explicit, implicit, heartbeat)
* - sendText with markdown stripping
* - sendMedia delegation to sendText
* - Error handling for missing accounts/channels
* - Abort signal handling
*/
import {
createMessageReceiptFromOutboundResults,
verifyChannelMessageAdapterCapabilityProofs,
} from "openclaw/plugin-sdk/channel-outbound";
import { describe, expect, it, vi } from "vitest";
import { resolveTwitchAccountContext } from "./config.js";
import { twitchMessageAdapter, twitchOutbound } from "./outbound.js";
import {
BASE_TWITCH_TEST_ACCOUNT,
installTwitchTestHooks,
makeTwitchTestConfig,
} from "./test-fixtures.js";
// Mock dependencies
vi.mock("./config.js", () => ({
DEFAULT_ACCOUNT_ID: "default",
resolveTwitchAccountContext: vi.fn(),
}));
vi.mock("./send.js", () => ({
sendMessageTwitchInternal: vi.fn(),
}));
vi.mock("./utils/markdown.js", () => ({
chunkTextForTwitch: vi.fn(chunkMockTextForTwitch),
}));
vi.mock("./utils/twitch.js", () => ({
normalizeTwitchChannel: (channel: string) => channel.toLowerCase().replace(/^#/, ""),
missingTargetError: (channel: string, hint: string) =>
new Error(`Missing target for ${channel}. Provide ${hint}`),
}));
function chunkMockTextForTwitch(text: string): string[] {
const chunks: string[] = [];
for (const chunk of text.split(/(.{500})/)) {
if (chunk.length > 0) {
chunks.push(chunk);
}
}
return chunks;
}
function assertResolvedTarget(
result: ReturnType<NonNullable<typeof twitchOutbound.resolveTarget>>,
): string {
if (!result.ok) {
throw result.error;
}
return result.to;
}
function expectTargetError(
resolveTarget: NonNullable<typeof twitchOutbound.resolveTarget>,
params: Parameters<NonNullable<typeof twitchOutbound.resolveTarget>>[0],
expectedMessage: string,
) {
const result = resolveTarget(params);
expect(result.ok).toBe(false);
if (result.ok) {
throw new Error("expected resolveTarget to fail");
}
expect(result.error.message).toContain(expectedMessage);
}
function twitchTestReceipt(messageId: string) {
return createMessageReceiptFromOutboundResults({
results: [
{
channel: "twitch",
conversationId: "testchannel",
messageId,
},
],
kind: "text",
});
}
describe("outbound", () => {
const mockAccount = {
...BASE_TWITCH_TEST_ACCOUNT,
accessToken: "oauth:test123",
};
const resolveTarget = twitchOutbound.resolveTarget!;
const mockConfig = makeTwitchTestConfig(mockAccount);
installTwitchTestHooks();
function setupAccountContext(params?: {
account?: typeof mockAccount | null;
availableAccountIds?: string[];
}) {
const account = params?.account === undefined ? mockAccount : params.account;
vi.mocked(resolveTwitchAccountContext).mockImplementation((_cfg, accountId) => ({
accountId: accountId?.trim() || "default",
account,
tokenResolution: { source: "config", token: account?.accessToken ?? "" },
configured: account !== null,
availableAccountIds: params?.availableAccountIds ?? ["default"],
}));
}
const abortedSendCases = [
{
name: "sendText",
invoke: (signal: AbortSignal) =>
twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello!",
accountId: "default",
signal,
} as Parameters<NonNullable<typeof twitchOutbound.sendText>>[0]),
},
{
name: "sendMedia",
invoke: (signal: AbortSignal) =>
twitchOutbound.sendMedia!({
cfg: mockConfig,
to: "#testchannel",
text: "Check this:",
mediaUrl: "https://example.com/image.png",
accountId: "default",
signal,
} as Parameters<NonNullable<typeof twitchOutbound.sendMedia>>[0]),
},
];
describe("abort handling", () => {
it.each(abortedSendCases)("$name should handle abort signal", async ({ invoke }) => {
const abortController = new AbortController();
abortController.abort();
await expect(invoke(abortController.signal)).rejects.toThrow("Outbound delivery aborted");
});
});
describe("metadata", () => {
it("should have direct delivery mode", () => {
expect(twitchOutbound.deliveryMode).toBe("direct");
});
it("should have 500 character text chunk limit", () => {
expect(twitchOutbound.textChunkLimit).toBe(500);
});
it("should chunk long messages at 500 characters", () => {
const chunker = twitchOutbound.chunker;
if (!chunker) {
throw new Error("twitch outbound.chunker unavailable");
}
expect(chunker("a".repeat(600), 500)).toEqual(["a".repeat(500), "a".repeat(100)]);
});
it("declares message adapter durable text and media with receipt proofs", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
setupAccountContext();
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: true,
messageId: "twitch-msg-123",
receipt: twitchTestReceipt("twitch-msg-123"),
});
const proofResults = await verifyChannelMessageAdapterCapabilityProofs({
adapterName: "twitch",
adapter: twitchMessageAdapter,
proofs: {
text: async () => {
const result = await twitchMessageAdapter.send?.text?.({
cfg: mockConfig,
to: "#testchannel",
text: "Hello Twitch!",
accountId: "default",
});
expect(result?.receipt?.platformMessageIds).toEqual(["twitch-msg-123"]);
},
media: async () => {
const result = await twitchMessageAdapter.send?.media?.({
cfg: mockConfig,
to: "#testchannel",
text: "image",
mediaUrl: "https://example.com/image.png",
accountId: "default",
});
expect(result?.receipt?.platformMessageIds).toEqual(["twitch-msg-123"]);
expect(sendMessageTwitchInternal).toHaveBeenLastCalledWith(
"testchannel",
"image https://example.com/image.png",
mockConfig,
"default",
true,
console,
);
},
messageSendingHooks: () => {
expect(twitchMessageAdapter.durableFinal?.capabilities?.messageSendingHooks).toBe(true);
},
},
});
expect(proofResults).toEqual([
{ capability: "text", status: "verified" },
{ capability: "media", status: "verified" },
{ capability: "poll", status: "not_declared" },
{ capability: "payload", status: "not_declared" },
{ capability: "silent", status: "not_declared" },
{ capability: "replyTo", status: "not_declared" },
{ capability: "thread", status: "not_declared" },
{ capability: "nativeQuote", status: "not_declared" },
{ capability: "messageSendingHooks", status: "verified" },
{ capability: "batch", status: "not_declared" },
{ capability: "reconcileUnknownSend", status: "not_declared" },
{ capability: "afterSendSuccess", status: "not_declared" },
{ capability: "afterCommit", status: "not_declared" },
]);
});
it("adapts outbound progress into message receipts", async () => {
const progress = {
channel: "twitch",
messageId: "twitch-progress-1",
receipt: twitchTestReceipt("twitch-progress-1"),
};
const sendText = twitchOutbound.sendText;
if (!sendText) {
throw new Error("Twitch text sending is not available.");
}
const sendSpy = vi.spyOn(twitchOutbound, "sendText").mockImplementationOnce(async (ctx) => {
await ctx.onDeliveryResult?.(progress);
return progress;
});
const onDeliveryResult = vi.fn();
try {
await twitchMessageAdapter.send?.text?.({
cfg: mockConfig,
to: "#testchannel",
text: "Hello Twitch!",
accountId: "default",
onDeliveryResult,
});
} finally {
sendSpy.mockRestore();
}
expect(onDeliveryResult).toHaveBeenCalledOnce();
expect(onDeliveryResult.mock.calls[0]?.[0]?.receipt.platformMessageIds).toEqual([
"twitch-progress-1",
]);
});
});
describe("resolveTarget", () => {
it("should normalize and return target in explicit mode", () => {
const result = resolveTarget({
to: "#MyChannel",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(true);
expect(assertResolvedTarget(result)).toBe("mychannel");
});
it("should return target in implicit mode with wildcard allowlist", () => {
const result = resolveTarget({
to: "#AnyChannel",
mode: "implicit",
allowFrom: ["*"],
});
expect(result.ok).toBe(true);
expect(assertResolvedTarget(result)).toBe("anychannel");
});
it("should return target in implicit mode when in allowlist", () => {
const result = resolveTarget({
to: "#allowed",
mode: "implicit",
allowFrom: ["#allowed", "#other"],
});
expect(result.ok).toBe(true);
expect(assertResolvedTarget(result)).toBe("allowed");
});
it("should error when target not in allowlist (implicit mode)", () => {
expectTargetError(
resolveTarget,
{
to: "#notallowed",
mode: "implicit",
allowFrom: ["#primary", "#secondary"],
},
"Twitch",
);
});
it("should accept any target when allowlist is empty", () => {
const result = resolveTarget({
to: "#anychannel",
mode: "heartbeat",
allowFrom: [],
});
expect(result.ok).toBe(true);
expect(assertResolvedTarget(result)).toBe("anychannel");
});
it("should error when no target provided with allowlist", () => {
expectTargetError(
resolveTarget,
{
to: undefined,
mode: "implicit",
allowFrom: ["#fallback", "#other"],
},
"Twitch",
);
});
it("should return error when no target and no allowlist", () => {
expectTargetError(
resolveTarget,
{
to: undefined,
mode: "explicit",
allowFrom: [],
},
"Missing target",
);
});
it("should handle whitespace-only target", () => {
expectTargetError(
resolveTarget,
{
to: " ",
mode: "explicit",
allowFrom: [],
},
"Missing target",
);
});
it("should error when target normalizes to empty string", () => {
expectTargetError(
resolveTarget,
{
to: "#",
mode: "explicit",
allowFrom: [],
},
"Twitch",
);
});
it("should filter wildcard from allowlist when checking membership", () => {
const result = resolveTarget({
to: "#mychannel",
mode: "implicit",
allowFrom: ["*", "#specific"],
});
// With wildcard, any target is accepted
expect(result.ok).toBe(true);
expect(assertResolvedTarget(result)).toBe("mychannel");
});
});
describe("sendText", () => {
it("should send message successfully", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
setupAccountContext();
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: true,
messageId: "twitch-msg-123",
receipt: twitchTestReceipt("twitch-msg-123"),
});
const result = await twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello Twitch!",
accountId: "default",
});
expect(result.channel).toBe("twitch");
expect(result.messageId).toBe("twitch-msg-123");
expect(result.receipt?.platformMessageIds).toEqual(["twitch-msg-123"]);
expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
"testchannel",
"Hello Twitch!",
mockConfig,
"default",
true,
console,
);
expect(result.timestamp).toBeGreaterThan(0);
});
it("should throw when account not found", async () => {
setupAccountContext({ account: null });
await expect(
twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello!",
accountId: "nonexistent",
}),
).rejects.toThrow("Twitch account not found: nonexistent");
});
it("should throw when no channel specified", async () => {
const accountWithoutChannel = { ...mockAccount, channel: undefined as unknown as string };
setupAccountContext({ account: accountWithoutChannel });
await expect(
twitchOutbound.sendText!({
cfg: mockConfig,
to: "",
text: "Hello!",
accountId: "default",
}),
).rejects.toThrow("No channel specified");
});
it("should use account channel when target not provided", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
setupAccountContext();
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: true,
messageId: "msg-456",
receipt: twitchTestReceipt("msg-456"),
});
await twitchOutbound.sendText!({
cfg: mockConfig,
to: "",
text: "Hello!",
accountId: "default",
});
expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
"testchannel",
"Hello!",
mockConfig,
"default",
true,
console,
);
});
it("uses configured defaultAccount when accountId is omitted", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
vi.mocked(resolveTwitchAccountContext)
.mockImplementationOnce(() => ({
accountId: "secondary",
account: {
...mockAccount,
channel: "secondary-channel",
},
tokenResolution: { source: "config", token: mockAccount.accessToken },
configured: true,
availableAccountIds: ["default", "secondary"],
}))
.mockImplementation((_cfg, accountId) => ({
accountId: accountId?.trim() || "secondary",
account: {
...mockAccount,
channel: "secondary-channel",
},
tokenResolution: { source: "config", token: mockAccount.accessToken },
configured: true,
availableAccountIds: ["default", "secondary"],
}));
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: true,
messageId: "msg-secondary",
receipt: twitchTestReceipt("msg-secondary"),
});
const defaultAccountConfig = {
channels: {
twitch: {
defaultAccount: "secondary",
},
},
} as typeof mockConfig;
await twitchOutbound.sendText!({
cfg: defaultAccountConfig,
to: "#secondary-channel",
text: "Hello!",
});
expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
"secondary-channel",
"Hello!",
defaultAccountConfig,
"secondary",
true,
console,
);
});
it("should throw on send failure", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
setupAccountContext();
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: false,
messageId: "failed-msg",
receipt: createMessageReceiptFromOutboundResults({ results: [] }),
error: "Connection lost",
});
await expect(
twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello!",
accountId: "default",
}),
).rejects.toThrow("Connection lost");
});
});
describe("sendMedia", () => {
it("should combine text and media URL", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
setupAccountContext();
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: true,
messageId: "media-msg-123",
receipt: twitchTestReceipt("media-msg-123"),
});
const result = await twitchOutbound.sendMedia!({
cfg: mockConfig,
to: "#testchannel",
text: "Check this:",
mediaUrl: "https://example.com/image.png",
accountId: "default",
});
expect(result.channel).toBe("twitch");
expect(result.messageId).toBe("media-msg-123");
expect(result.receipt?.platformMessageIds).toEqual(["media-msg-123"]);
expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
"testchannel",
"Check this: https://example.com/image.png",
mockConfig,
"default",
true,
console,
);
});
it("should send media URL only when no text", async () => {
const { sendMessageTwitchInternal } = await import("./send.js");
setupAccountContext();
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
ok: true,
messageId: "media-only-msg",
receipt: twitchTestReceipt("media-only-msg"),
});
await twitchOutbound.sendMedia!({
cfg: mockConfig,
to: "#testchannel",
text: "",
mediaUrl: "https://example.com/image.png",
accountId: "default",
});
expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
"testchannel",
"https://example.com/image.png",
mockConfig,
"default",
true,
console,
);
});
});
});