diff --git a/src/connectors/sources/gmail.ts b/src/connectors/sources/gmail.ts index 64611b50..e12242b3 100644 --- a/src/connectors/sources/gmail.ts +++ b/src/connectors/sources/gmail.ts @@ -25,9 +25,9 @@ type GmailConfig = { enabled?: boolean; format?: GmailMessageFormat; includeSpamTrash?: boolean; - labelIds?: string[]; + labelIds?: unknown; maxMessages?: number; - metadataHeaders?: string[]; + metadataHeaders?: unknown; pageSize?: number; query?: string; readOnlyOperations?: unknown[]; @@ -186,7 +186,7 @@ async function listGmailMessages( accessToken: string, options: { includeSpamTrash?: boolean; - labelIds?: string[]; + labelIds?: unknown; maxMessages: number; pageSize?: number; query?: string; @@ -246,7 +246,7 @@ async function getGmailMessage( messageId: string, options: { format: GmailMessageFormat; - metadataHeaders?: string[]; + metadataHeaders?: unknown; }, ): Promise { return await gmailApi( @@ -402,10 +402,15 @@ function clamp(value: number | undefined, min: number, max: number): number { return Math.max(min, Math.min(max, Math.trunc(value ?? min))); } -function normalizeStringArray(values: string[] | undefined): string[] { - return (values ?? []) - .map((value) => value.trim()) - .filter((value) => value.length > 0); +function normalizeStringArray(value: unknown): string[] { + return Array.isArray(value) + ? value + .filter( + (item): item is string => + typeof item === "string" && item.trim().length > 0, + ) + .map((item) => item.trim()) + : []; } function removeEmptyValues( diff --git a/test/connector-config-overrides.test.ts b/test/connector-config-overrides.test.ts index ba1b8153..5935d01e 100644 --- a/test/connector-config-overrides.test.ts +++ b/test/connector-config-overrides.test.ts @@ -10,16 +10,20 @@ import { afterEach, describe, expect, test, vi } from "vitest"; // `options.connectorConfig` overrides are merged on top of the on-disk config // — the bug this PR fixes, where gmail/slack/x silently ignored the overrides. // -// Each connector short-circuits before any network call: +// Most cases short-circuit before any network call: // 1. an `enabled` gate -> returns status "skipped" when disabled // 2. a credentials gate -> returns status "error" when the token env is unset // so the observable effect of the merge is which gate the run stops at, with no -// `fetch` involved. +// `fetch` involved. The malformed Gmail array test stubs `fetch` explicitly. const originalHome = process.env.HOME; const tempHomes: string[] = []; const CONNECTOR_ENV_KEYS = [ + "OPENWIKI_GMAIL_ACCESS_TOKEN", + "OPENWIKI_GMAIL_REFRESH_TOKEN", + "OPENWIKI_GMAIL_TOKEN_EXPIRES_AT", + "OPENWIKI_GMAIL_TOKEN_TYPE", "OPENWIKI_X_ACCESS_TOKEN", "OPENWIKI_SLACK_USER_TOKEN", ] as const; @@ -47,6 +51,7 @@ async function writeConnectorConfig( afterEach(async () => { vi.resetModules(); + vi.unstubAllGlobals(); if (originalHome === undefined) { delete process.env.HOME; @@ -168,4 +173,49 @@ describe("gmail connector honors options.connectorConfig", () => { // If the override were ignored (the bug), it would proceed past this gate. expect(result.status).toBe("skipped"); }); + + test("ignores malformed non-array list config instead of crashing", async () => { + const home = await createTempHome(); + await writeConnectorConfig(home, "google", { + enabled: true, + format: "metadata", + labelIds: "INBOX", + maxMessages: 1, + metadataHeaders: "Subject", + }); + const requests: string[] = []; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = input instanceof Request ? input.url : String(input); + requests.push(url); + + if (new URL(url).pathname.endsWith("/messages")) { + return new Response(JSON.stringify({ messages: [{ id: "msg-1" }] }), { + headers: { "Content-Type": "application/json" }, + status: 200, + }); + } + + return new Response(JSON.stringify({ id: "msg-1" }), { + headers: { "Content-Type": "application/json" }, + status: 200, + }); + }), + ); + const connector = await loadGmailConnector(home); + process.env.OPENWIKI_GMAIL_ACCESS_TOKEN = "gmail-access-token"; + process.env.OPENWIKI_GMAIL_REFRESH_TOKEN = "gmail-refresh-token"; + + const result = await connector.ingest(); + + expect(result.status).toBe("success"); + expect(requests).toHaveLength(2); + expect(new URL(requests[0] ?? "").searchParams.getAll("labelIds")).toEqual( + [], + ); + expect( + new URL(requests[1] ?? "").searchParams.getAll("metadataHeaders"), + ).toEqual([]); + }); });