Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions src/connectors/sources/gmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -186,7 +186,7 @@ async function listGmailMessages(
accessToken: string,
options: {
includeSpamTrash?: boolean;
labelIds?: string[];
labelIds?: unknown;
maxMessages: number;
pageSize?: number;
query?: string;
Expand Down Expand Up @@ -246,7 +246,7 @@ async function getGmailMessage(
messageId: string,
options: {
format: GmailMessageFormat;
metadataHeaders?: string[];
metadataHeaders?: unknown;
},
): Promise<unknown> {
return await gmailApi(
Expand Down Expand Up @@ -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(
Expand Down
54 changes: 52 additions & 2 deletions test/connector-config-overrides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,6 +51,7 @@ async function writeConnectorConfig(

afterEach(async () => {
vi.resetModules();
vi.unstubAllGlobals();

if (originalHome === undefined) {
delete process.env.HOME;
Expand Down Expand Up @@ -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([]);
});
});