mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:20:45 +00:00
fix(ci): trim slow task and gateway paths
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
|||||||
resolveMattermostAccount,
|
resolveMattermostAccount,
|
||||||
type ResolvedMattermostAccount,
|
type ResolvedMattermostAccount,
|
||||||
} from "./mattermost/accounts.js";
|
} from "./mattermost/accounts.js";
|
||||||
|
import type { MattermostSlashCommandConfig } from "./mattermost/slash-commands.js";
|
||||||
|
import type { MattermostConfig } from "./types.js";
|
||||||
|
|
||||||
export const mattermostMeta = {
|
export const mattermostMeta = {
|
||||||
id: "mattermost",
|
id: "mattermost",
|
||||||
@@ -25,6 +27,8 @@ export const mattermostMeta = {
|
|||||||
quickstartAllowFrom: true,
|
quickstartAllowFrom: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
|
||||||
|
|
||||||
export function normalizeMattermostAllowEntry(entry: string): string {
|
export function normalizeMattermostAllowEntry(entry: string): string {
|
||||||
return normalizeLowercaseStringOrEmpty(
|
return normalizeLowercaseStringOrEmpty(
|
||||||
entry
|
entry
|
||||||
@@ -46,6 +50,63 @@ export function formatMattermostAllowEntry(entry: string): string {
|
|||||||
return normalizeLowercaseStringOrEmpty(trimmed.replace(/^(mattermost|user):/i, ""));
|
return normalizeLowercaseStringOrEmpty(trimmed.replace(/^(mattermost|user):/i, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function collectMattermostSlashCallbackPaths(
|
||||||
|
raw?: Partial<MattermostSlashCommandConfig>,
|
||||||
|
): string[] {
|
||||||
|
const callbackPath = (() => {
|
||||||
|
const trimmed = raw?.callbackPath?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return DEFAULT_SLASH_CALLBACK_PATH;
|
||||||
|
}
|
||||||
|
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||||
|
})();
|
||||||
|
const callbackUrl = raw?.callbackUrl?.trim();
|
||||||
|
const paths = new Set<string>([callbackPath]);
|
||||||
|
if (callbackUrl) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(callbackUrl).pathname;
|
||||||
|
if (pathname) {
|
||||||
|
paths.add(pathname);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep the normalized callback path when the configured URL is invalid.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...paths];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMattermostGatewayAuthBypassPaths(cfg: {
|
||||||
|
channels?: Record<string, unknown>;
|
||||||
|
}): string[] {
|
||||||
|
const base = cfg.channels?.mattermost as MattermostConfig | undefined;
|
||||||
|
const callbackPaths = new Set(
|
||||||
|
collectMattermostSlashCallbackPaths(
|
||||||
|
base?.commands as Partial<MattermostSlashCommandConfig> | undefined,
|
||||||
|
).filter(
|
||||||
|
(path) =>
|
||||||
|
path === "/api/channels/mattermost/command" || path.startsWith("/api/channels/mattermost/"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const accounts = base?.accounts ?? {};
|
||||||
|
for (const account of Object.values(accounts)) {
|
||||||
|
const accountConfig =
|
||||||
|
account && typeof account === "object" && !Array.isArray(account)
|
||||||
|
? (account as {
|
||||||
|
commands?: Parameters<typeof collectMattermostSlashCallbackPaths>[0];
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
for (const path of collectMattermostSlashCallbackPaths(accountConfig?.commands)) {
|
||||||
|
if (
|
||||||
|
path === "/api/channels/mattermost/command" ||
|
||||||
|
path.startsWith("/api/channels/mattermost/")
|
||||||
|
) {
|
||||||
|
callbackPaths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...callbackPaths];
|
||||||
|
}
|
||||||
|
|
||||||
export const mattermostConfigAdapter = createScopedChannelConfigAdapter<ResolvedMattermostAccount>({
|
export const mattermostConfigAdapter = createScopedChannelConfigAdapter<ResolvedMattermostAccount>({
|
||||||
sectionKey: "mattermost",
|
sectionKey: "mattermost",
|
||||||
listAccountIds: listMattermostAccountIds,
|
listAccountIds: listMattermostAccountIds,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
isMattermostConfigured,
|
isMattermostConfigured,
|
||||||
mattermostConfigAdapter,
|
mattermostConfigAdapter,
|
||||||
mattermostMeta,
|
mattermostMeta,
|
||||||
|
resolveMattermostGatewayAuthBypassPaths,
|
||||||
} from "./channel-config-shared.js";
|
} from "./channel-config-shared.js";
|
||||||
import { MattermostChannelConfigSchema } from "./config-surface.js";
|
import { MattermostChannelConfigSchema } from "./config-surface.js";
|
||||||
import { type ResolvedMattermostAccount } from "./mattermost/accounts.js";
|
import { type ResolvedMattermostAccount } from "./mattermost/accounts.js";
|
||||||
@@ -29,6 +30,9 @@ export const mattermostSetupPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
|
|||||||
isConfigured: isMattermostConfigured,
|
isConfigured: isMattermostConfigured,
|
||||||
describeAccount: describeMattermostAccount,
|
describeAccount: describeMattermostAccount,
|
||||||
},
|
},
|
||||||
|
gateway: {
|
||||||
|
resolveGatewayAuthBypassPaths: ({ cfg }) => resolveMattermostGatewayAuthBypassPaths(cfg),
|
||||||
|
},
|
||||||
setup: mattermostSetupAdapter,
|
setup: mattermostSetupAdapter,
|
||||||
setupWizard: mattermostSetupWizard,
|
setupWizard: mattermostSetupWizard,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
mattermostConfigAdapter,
|
mattermostConfigAdapter,
|
||||||
mattermostMeta as meta,
|
mattermostMeta as meta,
|
||||||
normalizeMattermostAllowEntry as normalizeAllowEntry,
|
normalizeMattermostAllowEntry as normalizeAllowEntry,
|
||||||
|
resolveMattermostGatewayAuthBypassPaths,
|
||||||
} from "./channel-config-shared.js";
|
} from "./channel-config-shared.js";
|
||||||
import { MattermostChannelConfigSchema } from "./config-surface.js";
|
import { MattermostChannelConfigSchema } from "./config-surface.js";
|
||||||
import { mattermostDoctor } from "./doctor.js";
|
import { mattermostDoctor } from "./doctor.js";
|
||||||
@@ -41,7 +42,6 @@ import {
|
|||||||
resolveMattermostReplyToMode,
|
resolveMattermostReplyToMode,
|
||||||
type ResolvedMattermostAccount,
|
type ResolvedMattermostAccount,
|
||||||
} from "./mattermost/accounts.js";
|
} from "./mattermost/accounts.js";
|
||||||
import type { MattermostSlashCommandConfig } from "./mattermost/slash-commands.js";
|
|
||||||
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
|
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
|
||||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||||
import { resolveMattermostOutboundSessionRoute } from "./session-route.js";
|
import { resolveMattermostOutboundSessionRoute } from "./session-route.js";
|
||||||
@@ -51,33 +51,6 @@ import type { MattermostConfig } from "./types.js";
|
|||||||
|
|
||||||
const loadMattermostChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
|
const loadMattermostChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
|
||||||
|
|
||||||
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
|
|
||||||
|
|
||||||
function collectMattermostSlashCallbackPaths(
|
|
||||||
raw?: Partial<MattermostSlashCommandConfig>,
|
|
||||||
): string[] {
|
|
||||||
const callbackPath = (() => {
|
|
||||||
const trimmed = raw?.callbackPath?.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return DEFAULT_SLASH_CALLBACK_PATH;
|
|
||||||
}
|
|
||||||
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
||||||
})();
|
|
||||||
const callbackUrl = raw?.callbackUrl?.trim();
|
|
||||||
const paths = new Set<string>([callbackPath]);
|
|
||||||
if (callbackUrl) {
|
|
||||||
try {
|
|
||||||
const pathname = new URL(callbackUrl).pathname;
|
|
||||||
if (pathname) {
|
|
||||||
paths.add(pathname);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Keep the normalized callback path when the configured URL is invalid.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...paths];
|
|
||||||
}
|
|
||||||
|
|
||||||
const mattermostSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedMattermostAccount>({
|
const mattermostSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedMattermostAccount>({
|
||||||
channelKey: "mattermost",
|
channelKey: "mattermost",
|
||||||
resolveDmPolicy: (account) => account.config.dmPolicy,
|
resolveDmPolicy: (account) => account.config.dmPolicy,
|
||||||
@@ -375,36 +348,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
gateway: {
|
gateway: {
|
||||||
resolveGatewayAuthBypassPaths: ({ cfg }) => {
|
resolveGatewayAuthBypassPaths: ({ cfg }) => resolveMattermostGatewayAuthBypassPaths(cfg),
|
||||||
const base = cfg.channels?.mattermost;
|
|
||||||
const callbackPaths = new Set(
|
|
||||||
collectMattermostSlashCallbackPaths(
|
|
||||||
base?.commands as Partial<MattermostSlashCommandConfig> | undefined,
|
|
||||||
).filter(
|
|
||||||
(path) =>
|
|
||||||
path === "/api/channels/mattermost/command" ||
|
|
||||||
path.startsWith("/api/channels/mattermost/"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const accounts = base?.accounts ?? {};
|
|
||||||
for (const account of Object.values(accounts)) {
|
|
||||||
const accountConfig =
|
|
||||||
account && typeof account === "object" && !Array.isArray(account)
|
|
||||||
? (account as {
|
|
||||||
commands?: Parameters<typeof collectMattermostSlashCallbackPaths>[0];
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
for (const path of collectMattermostSlashCallbackPaths(accountConfig?.commands)) {
|
|
||||||
if (
|
|
||||||
path === "/api/channels/mattermost/command" ||
|
|
||||||
path.startsWith("/api/channels/mattermost/")
|
|
||||||
) {
|
|
||||||
callbackPaths.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...callbackPaths];
|
|
||||||
},
|
|
||||||
startAccount: async (ctx) => {
|
startAccount: async (ctx) => {
|
||||||
const account = ctx.account;
|
const account = ctx.account;
|
||||||
const statusSink = createAccountStatusSink({
|
const statusSink = createAccountStatusSink({
|
||||||
|
|||||||
@@ -189,12 +189,29 @@ const GATEWAY_PROBE_STATUS_BY_PATH = new Map<string, "live" | "ready">([
|
|||||||
["/ready", "ready"],
|
["/ready", "ready"],
|
||||||
["/readyz", "ready"],
|
["/readyz", "ready"],
|
||||||
]);
|
]);
|
||||||
|
const pluginGatewayAuthBypassPathsCache = new WeakMap<
|
||||||
|
OpenClawConfig,
|
||||||
|
Promise<ReadonlySet<string>>
|
||||||
|
>();
|
||||||
|
|
||||||
async function resolvePluginGatewayAuthBypassPaths(
|
async function resolvePluginGatewayAuthBypassPaths(
|
||||||
configSnapshot: OpenClawConfig,
|
configSnapshot: OpenClawConfig,
|
||||||
): Promise<Set<string>> {
|
): Promise<Set<string>> {
|
||||||
const paths = new Set<string>();
|
const paths = new Set<string>();
|
||||||
const { listBundledChannelPlugins } = await getBundledChannelsModule();
|
const configuredChannels = configSnapshot.channels;
|
||||||
for (const plugin of listBundledChannelPlugins()) {
|
if (!configuredChannels || Object.keys(configuredChannels).length === 0) {
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
const { getBundledChannelPlugin, getBundledChannelSetupPlugin } =
|
||||||
|
await getBundledChannelsModule();
|
||||||
|
for (const channelId of Object.keys(configuredChannels)) {
|
||||||
|
const setupPlugin = getBundledChannelSetupPlugin(channelId);
|
||||||
|
const plugin = setupPlugin?.gateway?.resolveGatewayAuthBypassPaths
|
||||||
|
? setupPlugin
|
||||||
|
: getBundledChannelPlugin(channelId);
|
||||||
|
if (!plugin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (const path of plugin.gateway?.resolveGatewayAuthBypassPaths?.({ cfg: configSnapshot }) ??
|
for (const path of plugin.gateway?.resolveGatewayAuthBypassPaths?.({ cfg: configSnapshot }) ??
|
||||||
[]) {
|
[]) {
|
||||||
if (typeof path === "string" && path.trim()) {
|
if (typeof path === "string" && path.trim()) {
|
||||||
@@ -205,6 +222,21 @@ async function resolvePluginGatewayAuthBypassPaths(
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCachedPluginGatewayAuthBypassPaths(
|
||||||
|
configSnapshot: OpenClawConfig,
|
||||||
|
): Promise<ReadonlySet<string>> {
|
||||||
|
const cached = pluginGatewayAuthBypassPathsCache.get(configSnapshot);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const resolved = resolvePluginGatewayAuthBypassPaths(configSnapshot).catch((error) => {
|
||||||
|
pluginGatewayAuthBypassPathsCache.delete(configSnapshot);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
pluginGatewayAuthBypassPathsCache.set(configSnapshot, resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
function isOpenAiModelsPath(pathname: string): boolean {
|
function isOpenAiModelsPath(pathname: string): boolean {
|
||||||
return pathname === "/v1/models" || pathname.startsWith("/v1/models/");
|
return pathname === "/v1/models" || pathname.startsWith("/v1/models/");
|
||||||
}
|
}
|
||||||
@@ -1032,7 +1064,7 @@ export function createGatewayHttpServer(opts: {
|
|||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
requestPath,
|
requestPath,
|
||||||
getGatewayAuthBypassPaths: () => resolvePluginGatewayAuthBypassPaths(configSnapshot),
|
getGatewayAuthBypassPaths: () => getCachedPluginGatewayAuthBypassPaths(configSnapshot),
|
||||||
pluginPathContext,
|
pluginPathContext,
|
||||||
handlePluginRequest,
|
handlePluginRequest,
|
||||||
shouldEnforcePluginGatewayAuth,
|
shouldEnforcePluginGatewayAuth,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ const { MediaFetchErrorMock } = vi.hoisted(() => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let applyMediaUnderstanding: typeof import("./apply.js").applyMediaUnderstanding;
|
let applyMediaUnderstanding: typeof import("./apply.js").applyMediaUnderstanding;
|
||||||
let clearMediaUnderstandingBinaryCacheForTests: () => void;
|
|
||||||
|
|
||||||
const TEMP_MEDIA_PREFIX = "openclaw-echo-transcript-test-";
|
const TEMP_MEDIA_PREFIX = "openclaw-echo-transcript-test-";
|
||||||
let suiteTempMediaRootDir = "";
|
let suiteTempMediaRootDir = "";
|
||||||
@@ -211,8 +210,6 @@ describe("applyMediaUnderstanding – echo transcript", () => {
|
|||||||
suiteTempMediaRootDir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX));
|
suiteTempMediaRootDir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX));
|
||||||
const mod = await import("./apply.js");
|
const mod = await import("./apply.js");
|
||||||
applyMediaUnderstanding = mod.applyMediaUnderstanding;
|
applyMediaUnderstanding = mod.applyMediaUnderstanding;
|
||||||
const runner = await import("./runner.js");
|
|
||||||
clearMediaUnderstandingBinaryCacheForTests = runner.clearMediaUnderstandingBinaryCacheForTests;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -224,7 +221,6 @@ describe("applyMediaUnderstanding – echo transcript", () => {
|
|||||||
runCommandWithTimeoutMock.mockReset();
|
runCommandWithTimeoutMock.mockReset();
|
||||||
mockDeliverOutboundPayloads.mockClear();
|
mockDeliverOutboundPayloads.mockClear();
|
||||||
mockDeliverOutboundPayloads.mockResolvedValue([{ channel: "whatsapp", messageId: "echo-1" }]);
|
mockDeliverOutboundPayloads.mockResolvedValue([{ channel: "whatsapp", messageId: "echo-1" }]);
|
||||||
clearMediaUnderstandingBinaryCacheForTests?.();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -317,7 +317,6 @@ describe("applyMediaUnderstanding", () => {
|
|||||||
contentType: "audio/ogg",
|
contentType: "audio/ogg",
|
||||||
fileName: "note.ogg",
|
fileName: "note.ogg",
|
||||||
});
|
});
|
||||||
clearMediaUnderstandingBinaryCacheForTests();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -653,6 +652,7 @@ describe("applyMediaUnderstanding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("auto-detects sherpa for audio when binary and model files are available", async () => {
|
it("auto-detects sherpa for audio when binary and model files are available", async () => {
|
||||||
|
clearMediaUnderstandingBinaryCacheForTests();
|
||||||
const binDir = await createTempMediaDir();
|
const binDir = await createTempMediaDir();
|
||||||
const modelDir = await createTempMediaDir();
|
const modelDir = await createTempMediaDir();
|
||||||
await createMockExecutable(binDir, "sherpa-onnx-offline");
|
await createMockExecutable(binDir, "sherpa-onnx-offline");
|
||||||
@@ -683,6 +683,7 @@ describe("applyMediaUnderstanding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("auto-detects whisper-cli when sherpa is unavailable", async () => {
|
it("auto-detects whisper-cli when sherpa is unavailable", async () => {
|
||||||
|
clearMediaUnderstandingBinaryCacheForTests();
|
||||||
const binDir = await createTempMediaDir();
|
const binDir = await createTempMediaDir();
|
||||||
const modelDir = await createTempMediaDir();
|
const modelDir = await createTempMediaDir();
|
||||||
await createMockExecutable(binDir, "whisper-cli");
|
await createMockExecutable(binDir, "whisper-cli");
|
||||||
@@ -711,6 +712,7 @@ describe("applyMediaUnderstanding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("transcodes non-wav audio before auto-detected whisper-cli runs", async () => {
|
it("transcodes non-wav audio before auto-detected whisper-cli runs", async () => {
|
||||||
|
clearMediaUnderstandingBinaryCacheForTests();
|
||||||
const binDir = await createTempMediaDir();
|
const binDir = await createTempMediaDir();
|
||||||
const modelDir = await createTempMediaDir();
|
const modelDir = await createTempMediaDir();
|
||||||
await createMockExecutable(binDir, "whisper-cli");
|
await createMockExecutable(binDir, "whisper-cli");
|
||||||
@@ -770,6 +772,7 @@ describe("applyMediaUnderstanding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("skips audio auto-detect when no supported binaries or provider keys are available", async () => {
|
it("skips audio auto-detect when no supported binaries or provider keys are available", async () => {
|
||||||
|
clearMediaUnderstandingBinaryCacheForTests();
|
||||||
const emptyBinDir = await createTempMediaDir();
|
const emptyBinDir = await createTempMediaDir();
|
||||||
const isolatedAgentDir = await createTempMediaDir();
|
const isolatedAgentDir = await createTempMediaDir();
|
||||||
const ctx = await createAudioCtx({
|
const ctx = await createAudioCtx({
|
||||||
|
|||||||
@@ -34,21 +34,26 @@ export function createChannelReplyPipeline(params: {
|
|||||||
const channelId = params.channel
|
const channelId = params.channel
|
||||||
? (normalizeChannelId(params.channel) ?? params.channel)
|
? (normalizeChannelId(params.channel) ?? params.channel)
|
||||||
: undefined;
|
: undefined;
|
||||||
const plugin = params.transformReplyPayload
|
let plugin: ReturnType<typeof getChannelPlugin> | undefined;
|
||||||
? undefined
|
let pluginTransformResolved = false;
|
||||||
|
const resolvePluginTransform = () => {
|
||||||
|
if (pluginTransformResolved) {
|
||||||
|
return plugin?.messaging?.transformReplyPayload;
|
||||||
|
}
|
||||||
|
pluginTransformResolved = true;
|
||||||
|
plugin = channelId ? getChannelPlugin(channelId) : undefined;
|
||||||
|
return plugin?.messaging?.transformReplyPayload;
|
||||||
|
};
|
||||||
|
const transformReplyPayload = params.transformReplyPayload
|
||||||
|
? params.transformReplyPayload
|
||||||
: channelId
|
: channelId
|
||||||
? getChannelPlugin(channelId)
|
|
||||||
: undefined;
|
|
||||||
const transformReplyPayload =
|
|
||||||
params.transformReplyPayload ??
|
|
||||||
(plugin?.messaging?.transformReplyPayload
|
|
||||||
? (payload: ReplyPayload) =>
|
? (payload: ReplyPayload) =>
|
||||||
plugin.messaging?.transformReplyPayload?.({
|
resolvePluginTransform()?.({
|
||||||
payload,
|
payload,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
}) ?? payload
|
}) ?? payload
|
||||||
: undefined);
|
: undefined;
|
||||||
return {
|
return {
|
||||||
...createReplyPrefixOptions({
|
...createReplyPrefixOptions({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export {
|
|||||||
} from "./session-chat-type-shared.js";
|
} from "./session-chat-type-shared.js";
|
||||||
|
|
||||||
export function deriveSessionChatType(sessionKey: string | undefined | null): SessionKeyChatType {
|
export function deriveSessionChatType(sessionKey: string | undefined | null): SessionKeyChatType {
|
||||||
|
const builtInType = deriveSessionChatTypeFromKey(sessionKey);
|
||||||
|
if (builtInType !== "unknown") {
|
||||||
|
return builtInType;
|
||||||
|
}
|
||||||
|
|
||||||
return deriveSessionChatTypeFromKey(
|
return deriveSessionChatTypeFromKey(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
Array.from(iterateBootstrapChannelPlugins())
|
Array.from(iterateBootstrapChannelPlugins())
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import type { AcpSessionStoreEntry } from "../acp/runtime/session-meta.js";
|
||||||
|
import type { SessionEntry } from "../config/sessions.js";
|
||||||
|
import type { ParsedAgentSessionKey } from "../routing/session-key.js";
|
||||||
|
import {
|
||||||
|
resetTaskRegistryMaintenanceRuntimeForTests,
|
||||||
|
runTaskRegistryMaintenance,
|
||||||
|
setTaskRegistryMaintenanceRuntimeForTests,
|
||||||
|
stopTaskRegistryMaintenanceForTests,
|
||||||
|
} from "./task-registry.maintenance.js";
|
||||||
import type { TaskRecord } from "./task-registry.types.js";
|
import type { TaskRecord } from "./task-registry.types.js";
|
||||||
|
|
||||||
const GRACE_EXPIRED_MS = 10 * 60_000;
|
const GRACE_EXPIRED_MS = 10 * 60_000;
|
||||||
@@ -22,54 +31,66 @@ function makeStaleTask(overrides: Partial<TaskRecord>): TaskRecord {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMaintenanceModule(params: {
|
type TaskRegistryMaintenanceRuntime = Parameters<
|
||||||
|
typeof setTaskRegistryMaintenanceRuntimeForTests
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
stopTaskRegistryMaintenanceForTests();
|
||||||
|
resetTaskRegistryMaintenanceRuntimeForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createTaskRegistryMaintenanceHarness(params: {
|
||||||
tasks: TaskRecord[];
|
tasks: TaskRecord[];
|
||||||
sessionStore?: Record<string, unknown>;
|
sessionStore?: Record<string, SessionEntry>;
|
||||||
acpEntry?: unknown;
|
acpEntry?: AcpSessionStoreEntry["entry"];
|
||||||
activeCronJobIds?: string[];
|
activeCronJobIds?: string[];
|
||||||
activeRunIds?: string[];
|
activeRunIds?: string[];
|
||||||
}) {
|
}) {
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const sessionStore = params.sessionStore ?? {};
|
const sessionStore = params.sessionStore ?? {};
|
||||||
const acpEntry = params.acpEntry;
|
const acpEntry = params.acpEntry;
|
||||||
const activeCronJobIds = new Set(params.activeCronJobIds ?? []);
|
const activeCronJobIds = new Set(params.activeCronJobIds ?? []);
|
||||||
const activeRunIds = new Set(params.activeRunIds ?? []);
|
const activeRunIds = new Set(params.activeRunIds ?? []);
|
||||||
const currentTasks = new Map(params.tasks.map((task) => [task.taskId, { ...task }]));
|
const currentTasks = new Map(params.tasks.map((task) => [task.taskId, { ...task }]));
|
||||||
|
|
||||||
vi.doMock("../acp/runtime/session-meta.js", () => ({
|
const runtime: TaskRegistryMaintenanceRuntime = {
|
||||||
readAcpSessionEntry: () =>
|
readAcpSessionEntry: () =>
|
||||||
acpEntry !== undefined
|
acpEntry !== undefined
|
||||||
? { entry: acpEntry, storeReadFailed: false }
|
? ({
|
||||||
: { entry: undefined, storeReadFailed: false },
|
cfg: {} as never,
|
||||||
}));
|
storePath: "",
|
||||||
|
sessionKey: "",
|
||||||
vi.doMock("../config/sessions.js", () => ({
|
storeSessionKey: "",
|
||||||
|
entry: acpEntry,
|
||||||
|
storeReadFailed: false,
|
||||||
|
} satisfies AcpSessionStoreEntry)
|
||||||
|
: ({
|
||||||
|
cfg: {} as never,
|
||||||
|
storePath: "",
|
||||||
|
sessionKey: "",
|
||||||
|
storeSessionKey: "",
|
||||||
|
entry: undefined,
|
||||||
|
storeReadFailed: false,
|
||||||
|
} satisfies AcpSessionStoreEntry),
|
||||||
loadSessionStore: () => sessionStore,
|
loadSessionStore: () => sessionStore,
|
||||||
resolveStorePath: () => "",
|
resolveStorePath: () => "",
|
||||||
}));
|
|
||||||
|
|
||||||
vi.doMock("../cron/active-jobs.js", () => ({
|
|
||||||
isCronJobActive: (jobId: string) => activeCronJobIds.has(jobId),
|
isCronJobActive: (jobId: string) => activeCronJobIds.has(jobId),
|
||||||
}));
|
|
||||||
|
|
||||||
vi.doMock("../infra/agent-events.js", () => ({
|
|
||||||
getAgentRunContext: (runId: string) =>
|
getAgentRunContext: (runId: string) =>
|
||||||
activeRunIds.has(runId) ? { sessionKey: "main" } : undefined,
|
activeRunIds.has(runId) ? { sessionKey: "main" } : undefined,
|
||||||
}));
|
parseAgentSessionKey: (sessionKey: string | null | undefined): ParsedAgentSessionKey | null => {
|
||||||
|
if (!sessionKey) {
|
||||||
vi.doMock("./runtime-internal.js", () => ({
|
return null;
|
||||||
|
}
|
||||||
|
const [kind, agentId, ...rest] = sessionKey.split(":");
|
||||||
|
return kind === "agent" && agentId && rest.length > 0
|
||||||
|
? { agentId, rest: rest.join(":") }
|
||||||
|
: null;
|
||||||
|
},
|
||||||
deleteTaskRecordById: (taskId: string) => currentTasks.delete(taskId),
|
deleteTaskRecordById: (taskId: string) => currentTasks.delete(taskId),
|
||||||
ensureTaskRegistryReady: () => {},
|
ensureTaskRegistryReady: () => {},
|
||||||
getTaskById: (taskId: string) => currentTasks.get(taskId),
|
getTaskById: (taskId: string) => currentTasks.get(taskId),
|
||||||
listTaskRecords: () => params.tasks,
|
listTaskRecords: () => Array.from(currentTasks.values()),
|
||||||
markTaskLostById: (patch: {
|
markTaskLostById: (patch) => {
|
||||||
taskId: string;
|
|
||||||
endedAt: number;
|
|
||||||
lastEventAt?: number;
|
|
||||||
error?: string;
|
|
||||||
cleanupAfter?: number;
|
|
||||||
}) => {
|
|
||||||
const current = currentTasks.get(patch.taskId);
|
const current = currentTasks.get(patch.taskId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return null;
|
return null;
|
||||||
@@ -85,9 +106,9 @@ async function loadMaintenanceModule(params: {
|
|||||||
currentTasks.set(patch.taskId, next);
|
currentTasks.set(patch.taskId, next);
|
||||||
return next;
|
return next;
|
||||||
},
|
},
|
||||||
maybeDeliverTaskTerminalUpdate: () => false,
|
maybeDeliverTaskTerminalUpdate: async () => null,
|
||||||
resolveTaskForLookupToken: () => undefined,
|
resolveTaskForLookupToken: () => undefined,
|
||||||
setTaskCleanupAfterById: (patch: { taskId: string; cleanupAfter: number }) => {
|
setTaskCleanupAfterById: (patch) => {
|
||||||
const current = currentTasks.get(patch.taskId);
|
const current = currentTasks.get(patch.taskId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return null;
|
return null;
|
||||||
@@ -96,10 +117,10 @@ async function loadMaintenanceModule(params: {
|
|||||||
currentTasks.set(patch.taskId, next);
|
currentTasks.set(patch.taskId, next);
|
||||||
return next;
|
return next;
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
|
||||||
const mod = await import("./task-registry.maintenance.js");
|
setTaskRegistryMaintenanceRuntimeForTests(runtime);
|
||||||
return { mod, currentTasks };
|
return { currentTasks };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("task-registry maintenance issue #60299", () => {
|
describe("task-registry maintenance issue #60299", () => {
|
||||||
@@ -111,12 +132,12 @@ describe("task-registry maintenance issue #60299", () => {
|
|||||||
childSessionKey,
|
childSessionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mod, currentTasks } = await loadMaintenanceModule({
|
const { currentTasks } = createTaskRegistryMaintenanceHarness({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
sessionStore: { [childSessionKey]: { updatedAt: Date.now() } },
|
sessionStore: { [childSessionKey]: { sessionId: childSessionKey, updatedAt: Date.now() } },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await mod.runTaskRegistryMaintenance()).toMatchObject({ reconciled: 1 });
|
expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: 1 });
|
||||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "lost" });
|
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "lost" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,12 +148,12 @@ describe("task-registry maintenance issue #60299", () => {
|
|||||||
childSessionKey: undefined,
|
childSessionKey: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mod, currentTasks } = await loadMaintenanceModule({
|
const { currentTasks } = createTaskRegistryMaintenanceHarness({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
activeCronJobIds: ["cron-job-2"],
|
activeCronJobIds: ["cron-job-2"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await mod.runTaskRegistryMaintenance()).toMatchObject({ reconciled: 0 });
|
expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: 0 });
|
||||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,12 +168,12 @@ describe("task-registry maintenance issue #60299", () => {
|
|||||||
childSessionKey: channelKey,
|
childSessionKey: channelKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mod, currentTasks } = await loadMaintenanceModule({
|
const { currentTasks } = createTaskRegistryMaintenanceHarness({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
sessionStore: { [channelKey]: { updatedAt: Date.now() } },
|
sessionStore: { [channelKey]: { sessionId: channelKey, updatedAt: Date.now() } },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await mod.runTaskRegistryMaintenance()).toMatchObject({ reconciled: 1 });
|
expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: 1 });
|
||||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "lost" });
|
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "lost" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,13 +188,13 @@ describe("task-registry maintenance issue #60299", () => {
|
|||||||
childSessionKey: channelKey,
|
childSessionKey: channelKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mod, currentTasks } = await loadMaintenanceModule({
|
const { currentTasks } = createTaskRegistryMaintenanceHarness({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
sessionStore: { [channelKey]: { updatedAt: Date.now() } },
|
sessionStore: { [channelKey]: { sessionId: channelKey, updatedAt: Date.now() } },
|
||||||
activeRunIds: ["run-chat-cli-live"],
|
activeRunIds: ["run-chat-cli-live"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await mod.runTaskRegistryMaintenance()).toMatchObject({ reconciled: 0 });
|
expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: 0 });
|
||||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user