fix: stabilize line and feishu ci shards

This commit is contained in:
Peter Steinberger 2026-04-06 01:44:58 +01:00
parent aeb9ad52fa
commit 3de91d9e01
No known key found for this signature in database
7 changed files with 148 additions and 32 deletions

View File

@ -89,6 +89,9 @@ describe("broadcast dispatch", () => {
routing: {
resolveAgentRoute: (params: unknown) => mockResolveAgentRoute(params),
},
session: {
resolveStorePath: vi.fn(() => "/tmp/feishu-session-store.json"),
},
reply: {
resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock,
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),

View File

@ -5,34 +5,146 @@ import type { LineAccountConfig } from "./types.js";
// Avoid pulling in globals/pairing/media dependencies; this suite only asserts
// allowlist/groupPolicy gating and message-context wiring.
vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime-env")>(
"openclaw/plugin-sdk/runtime-env",
);
return {
...actual,
danger: (text: string) => text,
logVerbose: () => {},
shouldLogVerbose: () => false,
};
});
vi.mock("openclaw/plugin-sdk/channel-inbound", () => ({
buildMentionRegexes: () => [],
matchesMentionPatterns: () => false,
resolveMentionGatingWithBypass: ({
isGroup,
requireMention,
canDetectMention,
wasMentioned,
hasAnyMention,
allowTextCommands,
hasControlCommand,
commandAuthorized,
}: {
isGroup: boolean;
requireMention: boolean;
canDetectMention: boolean;
wasMentioned: boolean;
hasAnyMention: boolean;
allowTextCommands: boolean;
hasControlCommand: boolean;
commandAuthorized: boolean;
}) => ({
shouldSkip:
isGroup &&
requireMention &&
canDetectMention &&
!wasMentioned &&
!(allowTextCommands && hasControlCommand && commandAuthorized && !hasAnyMention),
}),
}));
vi.mock("openclaw/plugin-sdk/channel-pairing", () => ({
createChannelPairingChallengeIssuer:
({ upsertPairingRequest }: { upsertPairingRequest: (args: unknown) => Promise<unknown> }) =>
async ({ senderId, onCreated }: { senderId: string; onCreated?: () => void }) => {
await upsertPairingRequest({ id: senderId, meta: {} });
onCreated?.();
},
}));
vi.mock("openclaw/plugin-sdk/command-auth", () => ({
hasControlCommand: (text: string) => text.trim().startsWith("!"),
resolveControlCommandGate: ({
hasControlCommand,
authorizers,
}: {
hasControlCommand: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
}) => ({
commandAuthorized:
hasControlCommand && authorizers.some((entry) => entry.allowed || entry.configured === false),
}),
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
resolveAllowlistProviderRuntimeGroupPolicy: ({
groupPolicy,
defaultGroupPolicy,
}: {
groupPolicy?: string;
defaultGroupPolicy: string;
}) => ({
groupPolicy: groupPolicy ?? defaultGroupPolicy,
providerMissingFallbackApplied: false,
}),
resolveDefaultGroupPolicy: (cfg: { channels?: { line?: { groupPolicy?: string } } }) =>
cfg.channels?.line?.groupPolicy ?? "open",
warnMissingProviderGroupPolicyFallbackOnce: () => {},
}));
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
danger: (text: string) => text,
logVerbose: () => {},
}));
vi.mock("openclaw/plugin-sdk/group-access", () => ({
evaluateMatchedGroupAccessForPolicy: ({
groupPolicy,
hasMatchInput,
allowlistConfigured,
allowlistMatched,
}: {
groupPolicy: string;
hasMatchInput: boolean;
allowlistConfigured: boolean;
allowlistMatched: boolean;
}) => {
if (groupPolicy === "disabled") {
return { allowed: false, reason: "disabled" };
}
if (groupPolicy !== "allowlist") {
return { allowed: true, reason: null };
}
if (!hasMatchInput) {
return { allowed: false, reason: "missing_match_input" };
}
if (!allowlistConfigured) {
return { allowed: false, reason: "empty_allowlist" };
}
if (!allowlistMatched) {
return { allowed: false, reason: "not_allowlisted" };
}
return { allowed: true, reason: null };
},
}));
vi.mock("openclaw/plugin-sdk/reply-history", () => ({
DEFAULT_GROUP_HISTORY_LIMIT: 20,
clearHistoryEntriesIfEnabled: ({
historyMap,
historyKey,
}: {
historyMap: Map<string, HistoryEntry[]>;
historyKey: string;
}) => {
historyMap.delete(historyKey);
},
recordPendingHistoryEntryIfEnabled: ({
historyMap,
historyKey,
limit,
entry,
}: {
historyMap: Map<string, HistoryEntry[]>;
historyKey: string;
limit: number;
entry: HistoryEntry;
}) => {
const existing = historyMap.get(historyKey) ?? [];
historyMap.set(historyKey, [...existing, entry].slice(-limit));
},
}));
vi.mock("openclaw/plugin-sdk/routing", () => ({
resolveAgentRoute: () => ({ agentId: "default" }),
}));
const { readAllowFromStoreMock, upsertPairingRequestMock } = vi.hoisted(() => ({
readAllowFromStoreMock: vi.fn(async () => [] as string[]),
upsertPairingRequestMock: vi.fn(async () => ({ code: "CODE", created: true })),
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/conversation-runtime")>(
"openclaw/plugin-sdk/conversation-runtime",
);
return {
...actual,
resolvePairingIdLabel: () => "lineUserId",
readChannelAllowFromStore: readAllowFromStoreMock,
upsertChannelPairingRequest: upsertPairingRequestMock,
};
});
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
resolvePairingIdLabel: () => "lineUserId",
readChannelAllowFromStore: readAllowFromStoreMock,
upsertChannelPairingRequest: upsertPairingRequestMock,
}));
vi.mock("./download.js", () => ({
downloadLineMedia: async () => {

View File

@ -10,7 +10,8 @@ import {
} from "./channel-api.js";
import { getLineRuntime } from "./runtime.js";
const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime.js"));
const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime.js"));
export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>["gateway"]> = {
startAccount: async (ctx) => {
@ -30,7 +31,7 @@ export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>[
let lineBotLabel = "";
try {
const probe = await (await loadLineChannelRuntime()).probeLineBot(token, 2500);
const probe = await (await loadLineProbeRuntime()).probeLineBot(token, 2500);
const displayName = probe.ok ? probe.bot?.displayName?.trim() : null;
if (displayName) {
lineBotLabel = ` (${displayName})`;
@ -45,7 +46,7 @@ export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>[
const monitorLineProvider =
getLineRuntime().channel.line?.monitorLineProvider ??
(await loadLineChannelRuntime()).monitorLineProvider;
(await loadLineMonitorRuntime()).monitorLineProvider;
return await monitorLineProvider({
channelAccessToken: token,

View File

@ -0,0 +1 @@
export { monitorLineProvider } from "./monitor.js";

View File

@ -0,0 +1 @@
export { probeLineBot } from "./probe.js";

View File

@ -28,7 +28,6 @@ vi.mock("@line/bot-sdk", () => ({
}));
const lineConfigure = createPluginSetupWizardConfigure(linePlugin);
let probeLineBot: typeof import("./probe.js").probeLineBot;
const LINE_SRC_PREFIX = `../../${bundledPluginRoot("line")}/src/`;
function normalizeModuleSpecifier(specifier: string): string | null {
@ -296,10 +295,6 @@ describe("line setup wizard", () => {
});
describe("probeLineBot", () => {
beforeAll(async () => {
({ probeLineBot } = await import("./probe.js"));
});
beforeEach(() => {
getBotInfoMock.mockReset();
MessagingApiClientMock.mockReset();
@ -315,6 +310,7 @@ describe("probeLineBot", () => {
});
it("returns timeout when bot info stalls", async () => {
const { probeLineBot } = await import("./probe.js");
vi.useFakeTimers();
getBotInfoMock.mockImplementation(() => new Promise(() => {}));
@ -327,6 +323,7 @@ describe("probeLineBot", () => {
});
it("returns bot info when available", async () => {
const { probeLineBot } = await import("./probe.js");
getBotInfoMock.mockResolvedValue({
displayName: "OpenClaw",
userId: "U123",
@ -343,6 +340,7 @@ describe("probeLineBot", () => {
describe("linePlugin status.probeAccount", () => {
it("falls back to the direct probe helper when runtime is not initialized", async () => {
const { probeLineBot } = await import("./probe.js");
MessagingApiClientMock.mockReset();
MessagingApiClientMock.mockImplementation(function () {
return { getBotInfo: getBotInfoMock };

View File

@ -8,7 +8,7 @@ import {
import { hasLineCredentials } from "./account-helpers.js";
import { DEFAULT_ACCOUNT_ID, type ChannelPlugin, type ResolvedLineAccount } from "./channel-api.js";
const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime.js"));
const collectLineStatusIssues = createDependentCredentialStatusIssueCollector({
channel: "line",
@ -23,7 +23,7 @@ export const lineStatusAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>["
collectStatusIssues: collectLineStatusIssues,
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
probeAccount: async ({ account, timeoutMs }) =>
await (await loadLineChannelRuntime()).probeLineBot(account.channelAccessToken, timeoutMs),
await (await loadLineProbeRuntime()).probeLineBot(account.channelAccessToken, timeoutMs),
resolveAccountSnapshot: ({ account }) => ({
accountId: account.accountId,
name: account.name,