fix(ci): trim slow task and gateway paths

This commit is contained in:
Vincent Koc
2026-04-16 13:34:14 -07:00
parent 56a9fd4b34
commit f835da1667
9 changed files with 191 additions and 120 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 () => {

View File

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

View File

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

View File

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

View File

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