From 02a116a77ea0c48a1c6491fbe285b0587ffb9776 Mon Sep 17 00:00:00 2001 From: Arnaud Prades Date: Sun, 20 Sep 2026 16:21:23 +0400 Subject: [PATCH] fix(auth): read the HttpOnly Slack d cookie for import-chrome import-chrome read the d cookie with document.cookie through AppleScript, but Slack sets that cookie HttpOnly, so the page context never sees it. Extraction failed on the first check and every failure collapsed into one message: "Could not extract tokens from Chrome." Read the cookie from Chrome's cookie store and decrypt it with the Chrome Safe Storage keychain password, matching the Brave and Slack-desktop paths, and surface the setup failures that used to be swallowed: Apple Events disabled, missing Automation consent, no Slack tab, and App-Bound Encryption (v20). import-chrome now also prints the workspace URLs it keyed tokens to, because a token stored against an Enterprise Grid org shell cannot serve workspace calls. Co-authored-by: Cursor Co-Authored-By: Paperclip --- README.md | 5 +- src/auth/chrome.ts | 365 ++++++++++++++++++++++++----- src/cli/auth-command.ts | 13 +- src/cli/context-client-resolver.ts | 12 +- test/canvas-create.test.ts | 2 +- test/channel-command.test.ts | 2 +- test/chrome-auth.test.ts | 114 +++++++++ test/message-draft-actions.test.ts | 2 +- test/message-send.test.ts | 2 +- test/search-command.test.ts | 2 +- 10 files changed, 448 insertions(+), 71 deletions(-) create mode 100644 test/chrome-auth.test.ts diff --git a/README.md b/README.md index 7fb9c07..119b81d 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,10 @@ agent-slack auth test ``` > [!NOTE] -> `import-brave` / `import-chrome` read tokens from a logged-in Slack tab via AppleScript. Both browsers ship with **Allow JavaScript from Apple Events** disabled by default — enable it in **View → Developer** before running these commands. macOS will prompt for your password the first time. +> `import-brave` / `import-chrome` read tokens from a logged-in Slack tab via AppleScript. Both browsers ship with **Allow JavaScript from Apple Events** disabled by default — enable it in **View → Developer** before running these commands. macOS will prompt for your password the first time. The first run also needs Automation consent for your terminal (**System Settings → Privacy & Security → Automation**) and access to the browser's `Safe Storage` keychain entry, which holds the key for the HttpOnly `d` cookie. + +> [!NOTE] +> Tokens are stored per workspace URL. On Enterprise Grid, import from a tab on the **workspace** URL (`https://.slack.com`) rather than the org shell (`https://.enterprise.slack.com`) so the stored entry matches the workspace you query. `agent-slack auth whoami` lists what each entry is keyed to. Alternatively, set env vars: diff --git a/src/auth/chrome.ts b/src/auth/chrome.ts index a31e9d6..2264d3a 100644 --- a/src/auth/chrome.ts +++ b/src/auth/chrome.ts @@ -1,5 +1,12 @@ -import { execSync } from "node:child_process"; -import { platform } from "node:os"; +import { execFileSync } from "node:child_process"; +import { copyFileSync, existsSync, readdirSync, unlinkSync } from "node:fs"; +import { randomUUID } from "node:crypto"; +import { homedir, platform, tmpdir } from "node:os"; +import { join } from "node:path"; +import { queryReadonlySqlite } from "./firefox-profile.ts"; +import { decryptChromiumCookieValue } from "./chromium-cookie.ts"; +import { getKeychainTimeoutMs } from "./keychain.ts"; +import { isRecord } from "../lib/object-type-guards.ts"; type ChromeExtractedTeam = { url: string; name?: string; token: string }; @@ -10,32 +17,86 @@ export type ChromeExtracted = { const IS_MACOS = platform() === "darwin"; -function escapeOsaScript(script: string): string { - // osascript -e '...' - return script.replace(/'/g, `'"'"'`); +// Chrome ships with `Allow JavaScript from Apple Events` OFF. Without it, +// `execute javascript` from osascript fails, and that is how this module reads +// `localStorage.localConfig_v2` out of a Slack tab. +export class ChromeAppleScriptDisabledError extends Error { + constructor() { + super( + "Chrome is blocking JavaScript from Apple Events.\n" + + "Enable it: Chrome menu → View → Developer → Allow JavaScript from Apple Events\n" + + "Then re-run: agent-slack auth import-chrome", + ); + this.name = "ChromeAppleScriptDisabledError"; + } } -function osascript(script: string): string { - return execSync(`osascript -e '${escapeOsaScript(script)}'`, { - encoding: "utf8", - timeout: 7000, - stdio: ["ignore", "pipe", "pipe"], - }).trim(); +// macOS gates Apple events behind per-app Automation consent. A denied or +// never-prompted terminal gets -1743 with no visible prompt. +export class ChromeAutomationNotAuthorizedError extends Error { + constructor() { + super( + "This terminal is not authorized to send Apple events to Google Chrome.\n" + + "Allow it: System Settings → Privacy & Security → Automation → → Google Chrome\n" + + "Then re-run: agent-slack auth import-chrome", + ); + this.name = "ChromeAutomationNotAuthorizedError"; + } } -function cookieScript(): string { - return ` - tell application "Google Chrome" - repeat with w in windows - repeat with t in tabs of w - if URL of t contains "slack.com" then - return execute t javascript "document.cookie.split('; ').find(c => c.startsWith('d='))?.split('=')[1] || ''" - end if - end repeat - end repeat - return "" - end tell - `; +export class ChromeNoSlackTabError extends Error { + constructor() { + super( + "No Slack tab found in Google Chrome.\n" + + "Open your workspace in Chrome (for Enterprise Grid use the workspace URL, " + + "e.g. https://.slack.com, not the https://.enterprise.slack.com shell), " + + "sign in, then re-run: agent-slack auth import-chrome", + ); + this.name = "ChromeNoSlackTabError"; + } +} + +// Chrome 127+ on macOS/Windows can wrap cookie values with App-Bound +// Encryption (`v20`), which the Safe Storage keychain password cannot open. +export class ChromeAppBoundEncryptionError extends Error { + constructor() { + super( + "Chrome encrypted the Slack cookie with App-Bound Encryption (v20), which cannot be decrypted outside Chrome.\n" + + "Use another import path instead: agent-slack auth import-desktop (Slack app), " + + "auth import-brave, or auth import-firefox.", + ); + this.name = "ChromeAppBoundEncryptionError"; + } +} + +// --- AppleScript helpers (for extracting teams from Chrome tabs) --- + +const APPLESCRIPT_JS_DISABLED_MARKER = "Executing JavaScript through AppleScript is turned off"; +const APPLESCRIPT_NOT_AUTHORIZED_MARKER = "-1743"; +const NO_SLACK_TAB_SENTINEL = "__AGENT_SLACK_NO_TAB__"; + +function osascript(script: string): string { + try { + return execFileSync("osascript", ["-e", script], { + encoding: "utf8", + timeout: 7000, + stdio: ["ignore", "pipe", "pipe"], + }).trim(); + } catch (err: unknown) { + const stderr = + err && typeof err === "object" && "stderr" in err + ? String((err as { stderr: unknown }).stderr ?? "") + : ""; + const message = err instanceof Error ? err.message : String(err); + const combined = `${stderr}\n${message}`; + if (combined.includes(APPLESCRIPT_JS_DISABLED_MARKER)) { + throw new ChromeAppleScriptDisabledError(); + } + if (combined.includes(APPLESCRIPT_NOT_AUTHORIZED_MARKER)) { + throw new ChromeAutomationNotAuthorizedError(); + } + throw err; + } } const TEAM_JSON_PATHS = [ @@ -46,23 +107,6 @@ const TEAM_JSON_PATHS = [ "JSON.stringify(window.boot_data?.teams || {})", ]; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function toChromeTeam(value: unknown): ChromeExtractedTeam | null { - if (!isRecord(value)) { - return null; - } - const token = typeof value.token === "string" ? value.token : null; - const url = typeof value.url === "string" ? value.url : null; - if (!token || !url || !token.startsWith("xoxc-")) { - return null; - } - const name = typeof value.name === "string" ? value.name : undefined; - return { url, name, token }; -} - function teamsScript(): string { const tryPaths = TEAM_JSON_PATHS.map( (expr) => `try { var v = ${expr}; if (v && v !== '{}' && v !== 'null') return v; } catch(e) {}`, @@ -76,39 +120,242 @@ function teamsScript(): string { end if end repeat end repeat - return "{}" + return "${NO_SLACK_TAB_SENTINEL}" end tell `; } -export function extractFromChrome(): ChromeExtracted | null { - if (!IS_MACOS) { +function toChromeTeam(value: unknown): ChromeExtractedTeam | null { + if (!isRecord(value)) { + return null; + } + const token = typeof value.token === "string" ? value.token : null; + const url = typeof value.url === "string" ? value.url : null; + if (!token || !url || !token.startsWith("xoxc-")) { return null; } + const name = typeof value.name === "string" ? value.name : undefined; + return { url, name, token }; +} + +export function parseChromeTeams(teamsRaw: string): ChromeExtractedTeam[] { + if (teamsRaw.includes(NO_SLACK_TAB_SENTINEL)) { + throw new ChromeNoSlackTabError(); + } + + let teamsObj: unknown = {}; try { - const cookie = osascript(cookieScript()); - if (!cookie || !cookie.startsWith("xoxd-")) { - return null; + teamsObj = JSON.parse(teamsRaw || "{}"); + } catch { + teamsObj = {}; + } + + const teamsRecord = isRecord(teamsObj) ? teamsObj : {}; + return Object.values(teamsRecord) + .map((t) => toChromeTeam(t)) + .filter((t): t is ChromeExtractedTeam => t !== null); +} + +function extractTeamsFromChromeTab(): ChromeExtractedTeam[] { + return parseChromeTeams(osascript(teamsScript())); +} + +// --- Cookie extraction from Chrome's SQLite database --- +// +// The Slack `d` cookie is HttpOnly, so `document.cookie` in the page context +// never sees it. It has to come from Chrome's cookie store, decrypted with the +// `Chrome Safe Storage` keychain password. + +const CHROME_USER_DATA_DIR = join(homedir(), "Library", "Application Support", "Google", "Chrome"); + +export function getChromeProfileDirs(userDataDir: string): string[] { + const profiles: string[] = []; + if (existsSync(join(userDataDir, "Default"))) { + profiles.push("Default"); + } + try { + for (const entry of readdirSync(userDataDir).sort()) { + if (/^Profile \d+$/.test(entry)) { + profiles.push(entry); + } } + } catch { + // user data dir may not exist + } + return profiles; +} - const teamsRaw = osascript(teamsScript()); - let teamsObj: unknown = {}; +export function getChromeCookieDbPaths(userDataDir: string): string[] { + const paths: string[] = []; + for (const profile of getChromeProfileDirs(userDataDir)) { + for (const candidate of [ + join(userDataDir, profile, "Network", "Cookies"), + join(userDataDir, profile, "Cookies"), + ]) { + if (existsSync(candidate)) { + paths.push(candidate); + } + } + } + return paths; +} + +function getChromeSafeStoragePasswords(): string[] { + const services = ["Chrome Safe Storage", "Chromium Safe Storage"]; + const passwords: string[] = []; + for (const service of services) { try { - teamsObj = JSON.parse(teamsRaw || "{}"); + const out = execFileSync("security", ["find-generic-password", "-w", "-s", service], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: getKeychainTimeoutMs(), + }).trim(); + if (out) { + passwords.push(out); + } } catch { - teamsObj = {}; + // continue } + } + return [...new Set(passwords)]; +} - const teamsRecord = isRecord(teamsObj) ? teamsObj : {}; - const teams: ChromeExtractedTeam[] = Object.values(teamsRecord) - .map((t) => toChromeTeam(t)) - .filter((t): t is ChromeExtractedTeam => t !== null); +export type ChromeCookieRow = { + host_key: string; + name: string; + value: string; + encrypted_value: Uint8Array; +}; + +/** + * Decrypt one Chrome cookie row into an `xoxd-*` token. + * + * Exported for tests; throws `ChromeAppBoundEncryptionError` on `v20` values. + */ +export function decryptSlackCookieRow(row: ChromeCookieRow, passwords: string[]): string | null { + if (row.value && row.value.startsWith("xoxd-")) { + return row.value; + } + + const encrypted = Buffer.from(row.encrypted_value || []); + if (encrypted.length === 0) { + return null; + } + + const prefix = encrypted.subarray(0, 3).toString("utf8"); + if (prefix === "v20") { + throw new ChromeAppBoundEncryptionError(); + } - if (teams.length === 0) { - return null; + const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted; + for (const password of passwords) { + try { + const decrypted = decryptChromiumCookieValue(data, { password, iterations: 1003 }); + const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/); + if (match) { + return match[0]!; + } + } catch { + // continue } - return { cookie_d: cookie, teams }; - } catch { + } + return null; +} + +/** + * Read Chrome's cookie DB through a copy. + * + * Chrome keeps the live DB open, so a readonly handle on it can fail or miss + * writes still sitting in the write-ahead log. Copying `Cookies` plus its + * `-wal`/`-shm` siblings gives a consistent, lock-free snapshot. + */ +async function querySlackCookieRows(dbPath: string): Promise { + const snapshot = join(tmpdir(), `agent-slack-chrome-cookies-${randomUUID()}`); + const sidecars = ["-wal", "-shm"]; + copyFileSync(dbPath, snapshot); + for (const suffix of sidecars) { + if (existsSync(`${dbPath}${suffix}`)) { + try { + copyFileSync(`${dbPath}${suffix}`, `${snapshot}${suffix}`); + } catch { + // sidecar is best effort + } + } + } + + try { + return (await queryReadonlySqlite( + snapshot, + "select host_key, name, value, encrypted_value from cookies where name = 'd' and host_key like '%slack.com' order by length(encrypted_value) desc", + )) as ChromeCookieRow[]; + } finally { + for (const path of [snapshot, ...sidecars.map((suffix) => `${snapshot}${suffix}`)]) { + try { + unlinkSync(path); + } catch { + // ignore + } + } + } +} + +async function extractCookieDFromChrome(): Promise { + const dbPaths = getChromeCookieDbPaths(CHROME_USER_DATA_DIR); + if (dbPaths.length === 0) { + throw new Error(`Chrome Cookies DB not found under ${CHROME_USER_DATA_DIR}`); + } + + const passwords = getChromeSafeStoragePasswords(); + const errors: string[] = []; + + for (const dbPath of dbPaths) { + let rows: ChromeCookieRow[]; + try { + rows = await querySlackCookieRows(dbPath); + } catch (err: unknown) { + errors.push(`${dbPath}: ${err instanceof Error ? err.message : String(err)}`); + continue; + } + + if (!rows || rows.length === 0) { + errors.push(`${dbPath}: no Slack 'd' cookie`); + continue; + } + + for (const row of rows) { + // App-Bound Encryption is a dead end for every profile, so let it escape. + const cookie = decryptSlackCookieRow(row, passwords); + if (cookie) { + return cookie; + } + } + errors.push( + passwords.length === 0 + ? `${dbPath}: no Chrome Safe Storage password available from the keychain` + : `${dbPath}: could not decrypt Slack 'd' cookie`, + ); + } + + throw new Error(`Could not read the Slack 'd' cookie from Chrome:\n - ${errors.join("\n - ")}`); +} + +// --- Main export --- + +export async function extractFromChrome(): Promise { + if (!IS_MACOS) { return null; } + + // Actionable failures propagate; only an unrecognized shape returns null. + const teams = extractTeamsFromChromeTab(); + if (teams.length === 0) { + return null; + } + + const cookie_d = await extractCookieDFromChrome(); + if (!cookie_d || !cookie_d.startsWith("xoxd-")) { + return null; + } + + return { cookie_d, teams }; } diff --git a/src/cli/auth-command.ts b/src/cli/auth-command.ts index f4f3127..87df7ae 100644 --- a/src/cli/auth-command.ts +++ b/src/cli/auth-command.ts @@ -78,16 +78,18 @@ export function registerAuthCommand(input: { program: Command; ctx: CliContext } .description("Import xoxc/xoxd from a logged-in Slack tab in Google Chrome (macOS)") .action(async () => { try { - const extracted = input.ctx.importChrome(); + const extracted = await input.ctx.importChrome(); if (!extracted) { throw new Error( "Could not extract tokens from Chrome. Open Slack in Chrome and ensure you're logged in.", ); } + const importedUrls: string[] = []; for (const team of extracted.teams) { + const workspaceUrl = input.ctx.normalizeUrl(team.url); await upsertWorkspace({ - workspace_url: input.ctx.normalizeUrl(team.url), + workspace_url: workspaceUrl, workspace_name: team.name, auth: { auth_type: "browser", @@ -95,8 +97,13 @@ export function registerAuthCommand(input: { program: Command; ctx: CliContext } xoxd_cookie: extracted.cookie_d, }, }); + importedUrls.push(workspaceUrl); } - console.log(`Imported ${extracted.teams.length} workspace token(s) from Chrome.`); + // Print the URLs: tokens are keyed by workspace URL, and on Enterprise + // Grid an enterprise-scoped entry cannot serve workspace endpoints. + console.log( + `Imported ${extracted.teams.length} workspace token(s) from Chrome:\n - ${importedUrls.join("\n - ")}`, + ); } catch (err: unknown) { console.error(input.ctx.errorMessage(err)); process.exitCode = 1; diff --git a/src/cli/context-client-resolver.ts b/src/cli/context-client-resolver.ts index d9beb8b..5351fcf 100644 --- a/src/cli/context-client-resolver.ts +++ b/src/cli/context-client-resolver.ts @@ -140,9 +140,15 @@ export async function getClientForWorkspace(workspaceUrl?: string): Promise<{ cookie_d: string; teams: { url: string; name?: string; token: string }[]; }[] = []; - const chromeResult = extractFromChrome(); - if (chromeResult && chromeResult.teams.length > 0) { - browserSources.push(chromeResult); + try { + const chromeResult = await extractFromChrome(); + if (chromeResult && chromeResult.teams.length > 0) { + browserSources.push(chromeResult); + } + } catch { + // Chrome is an opportunistic fallback here: its setup errors (no Slack tab, + // Apple Events disabled, App-Bound Encryption) are actionable only for the + // explicit `auth import-chrome` command, not for every read command. } try { const braveResult = await extractFromBrave(); diff --git a/test/canvas-create.test.ts b/test/canvas-create.test.ts index e904eb2..f669f0f 100644 --- a/test/canvas-create.test.ts +++ b/test/canvas-create.test.ts @@ -63,7 +63,7 @@ function createContext( teams: [], source: { leveldb_path: "", cookies_path: "" }, }), - importChrome: () => ({ cookie_d: "", teams: [] }), + importChrome: async () => ({ cookie_d: "", teams: [] }), importBrave: async () => null, importFirefox: async () => null, }; diff --git a/test/channel-command.test.ts b/test/channel-command.test.ts index 2759edc..6984cef 100644 --- a/test/channel-command.test.ts +++ b/test/channel-command.test.ts @@ -55,7 +55,7 @@ function createContext() { teams: [], source: { leveldb_path: "", cookies_path: "" }, }), - importChrome: () => ({ cookie_d: "", teams: [] }), + importChrome: async () => ({ cookie_d: "", teams: [] }), importBrave: async () => null, importFirefox: async () => null, }; diff --git a/test/chrome-auth.test.ts b/test/chrome-auth.test.ts new file mode 100644 index 0000000..d0000ff --- /dev/null +++ b/test/chrome-auth.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, test } from "bun:test"; +import { createCipheriv, pbkdf2Sync } from "node:crypto"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + ChromeAppBoundEncryptionError, + ChromeNoSlackTabError, + decryptSlackCookieRow, + getChromeCookieDbPaths, + getChromeProfileDirs, + parseChromeTeams, + type ChromeCookieRow, +} from "../src/auth/chrome.ts"; + +const PASSWORD = "chrome-safe-storage-password"; + +function encryptChromiumCookie(plaintext: string, prefix: string): Uint8Array { + const key = pbkdf2Sync(PASSWORD, Buffer.from("saltysalt", "utf8"), 1003, 16, "sha1"); + const cipher = createCipheriv("aes-128-cbc", key, Buffer.alloc(16, " ")); + const body = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + return Buffer.concat([Buffer.from(prefix, "utf8"), body]); +} + +function makeRow(overrides: Partial): ChromeCookieRow { + return { + host_key: ".slack.com", + name: "d", + value: "", + encrypted_value: new Uint8Array(), + ...overrides, + }; +} + +describe("parseChromeTeams", () => { + test("throws an actionable error when no Slack tab is open", () => { + expect(() => parseChromeTeams("__AGENT_SLACK_NO_TAB__")).toThrow(ChromeNoSlackTabError); + }); + + test("keeps only xoxc entries that carry a workspace URL", () => { + const raw = JSON.stringify({ + TS7LN8J6M: { url: "https://gpcorporate.slack.com/", name: "GP", token: "xoxc-workspace" }, + E04N67U6VNC: { url: "https://gp.enterprise.slack.com/", token: "xoxc-enterprise" }, + bogus: { url: "https://other.slack.com/", token: "xoxb-not-a-client-token" }, + noUrl: { token: "xoxc-orphan" }, + }); + + expect(parseChromeTeams(raw)).toEqual([ + { url: "https://gpcorporate.slack.com/", name: "GP", token: "xoxc-workspace" }, + { url: "https://gp.enterprise.slack.com/", name: undefined, token: "xoxc-enterprise" }, + ]); + }); + + test("returns no teams for unusable payloads", () => { + expect(parseChromeTeams("{}")).toEqual([]); + expect(parseChromeTeams("not json")).toEqual([]); + }); +}); + +describe("decryptSlackCookieRow", () => { + test("prefers an already plaintext value", () => { + const row = makeRow({ value: "xoxd-plaintext" }); + expect(decryptSlackCookieRow(row, [])).toBe("xoxd-plaintext"); + }); + + test("decrypts a v10 Safe Storage value", () => { + const row = makeRow({ encrypted_value: encryptChromiumCookie("xoxd-secret-token", "v10") }); + expect(decryptSlackCookieRow(row, ["wrong-password", PASSWORD])).toBe("xoxd-secret-token"); + }); + + test("percent-decodes the stored cookie", () => { + const row = makeRow({ encrypted_value: encryptChromiumCookie("xoxd-a%2Fb", "v10") }); + expect(decryptSlackCookieRow(row, [PASSWORD])).toBe("xoxd-a/b"); + }); + + test("reports App-Bound Encryption instead of failing silently", () => { + const row = makeRow({ encrypted_value: encryptChromiumCookie("xoxd-secret-token", "v20") }); + expect(() => decryptSlackCookieRow(row, [PASSWORD])).toThrow(ChromeAppBoundEncryptionError); + }); + + test("returns null when no password opens the value", () => { + const row = makeRow({ encrypted_value: encryptChromiumCookie("xoxd-secret-token", "v10") }); + expect(decryptSlackCookieRow(row, ["wrong-password"])).toBeNull(); + }); + + test("returns null for an empty encrypted value", () => { + expect(decryptSlackCookieRow(makeRow({}), [PASSWORD])).toBeNull(); + }); +}); + +describe("Chrome profile discovery", () => { + test("finds Default plus numbered profiles and both cookie locations", () => { + const userDataDir = mkdtempSync(join(tmpdir(), "agent-slack-chrome-test-")); + mkdirSync(join(userDataDir, "Default", "Network"), { recursive: true }); + mkdirSync(join(userDataDir, "Profile 1"), { recursive: true }); + mkdirSync(join(userDataDir, "Crashpad"), { recursive: true }); + writeFileSync(join(userDataDir, "Default", "Cookies"), ""); + writeFileSync(join(userDataDir, "Default", "Network", "Cookies"), ""); + writeFileSync(join(userDataDir, "Profile 1", "Cookies"), ""); + + expect(getChromeProfileDirs(userDataDir)).toEqual(["Default", "Profile 1"]); + expect(getChromeCookieDbPaths(userDataDir)).toEqual([ + join(userDataDir, "Default", "Network", "Cookies"), + join(userDataDir, "Default", "Cookies"), + join(userDataDir, "Profile 1", "Cookies"), + ]); + }); + + test("returns nothing for a missing user data dir", () => { + const missing = join(tmpdir(), "agent-slack-chrome-absent-dir"); + expect(getChromeProfileDirs(missing)).toEqual([]); + expect(getChromeCookieDbPaths(missing)).toEqual([]); + }); +}); diff --git a/test/message-draft-actions.test.ts b/test/message-draft-actions.test.ts index c636a56..cf2b35d 100644 --- a/test/message-draft-actions.test.ts +++ b/test/message-draft-actions.test.ts @@ -122,7 +122,7 @@ function createContext( teams: [], source: { leveldb_path: "", cookies_path: "" }, }), - importChrome: () => ({ cookie_d: "", teams: [] }), + importChrome: async () => ({ cookie_d: "", teams: [] }), importBrave: async () => null, importFirefox: async () => null, } satisfies CliContext; diff --git a/test/message-send.test.ts b/test/message-send.test.ts index 12ff5e3..ab1df0f 100644 --- a/test/message-send.test.ts +++ b/test/message-send.test.ts @@ -95,7 +95,7 @@ function createContext( teams: [], source: { leveldb_path: "", cookies_path: "" }, }), - importChrome: () => ({ cookie_d: "", teams: [] }), + importChrome: async () => ({ cookie_d: "", teams: [] }), importBrave: async () => null, importFirefox: async () => null, } satisfies CliContext; diff --git a/test/search-command.test.ts b/test/search-command.test.ts index f918ce9..074e31a 100644 --- a/test/search-command.test.ts +++ b/test/search-command.test.ts @@ -100,7 +100,7 @@ function createContext(calls: ApiCall[]): CliContext { teams: [], source: { leveldb_path: "", cookies_path: "" }, }), - importChrome: () => ({ cookie_d: "", teams: [] }), + importChrome: async () => ({ cookie_d: "", teams: [] }), importBrave: async () => null, importFirefox: async () => null, };