From 681d05d7eee2828dca3effc4e577f7cf4b941086 Mon Sep 17 00:00:00 2001 From: Pejman Pour-Moezzi Date: Wed, 8 Apr 2026 14:31:17 -0700 Subject: [PATCH 1/2] last30days: lazy-load vendored Bird cookie support --- scripts/lib/vendor/bird-search/lib/cookies.js | 24 ++++++++- tests/test_bird_x.py | 51 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/scripts/lib/vendor/bird-search/lib/cookies.js b/scripts/lib/vendor/bird-search/lib/cookies.js index 49789a1..636fc82 100644 --- a/scripts/lib/vendor/bird-search/lib/cookies.js +++ b/scripts/lib/vendor/bird-search/lib/cookies.js @@ -2,11 +2,13 @@ * Browser cookie extraction for Twitter authentication. * Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads. */ -import { getCookies } from '@steipete/sweet-cookie'; const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0']; const TWITTER_URL = 'https://x.com/'; const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/']; const DEFAULT_COOKIE_TIMEOUT_MS = 30_000; +async function loadSweetCookie() { + return import('@steipete/sweet-cookie'); +} function normalizeValue(value) { if (typeof value !== 'string') { return null; @@ -79,6 +81,7 @@ function pickCookieValue(cookies, name) { async function readTwitterCookiesFromBrowser(options) { const warnings = []; const out = buildEmpty(); + const { getCookies } = options; const { cookies, warnings: providerWarnings } = await getCookies({ url: TWITTER_URL, origins: TWITTER_ORIGINS, @@ -165,8 +168,27 @@ export async function resolveCredentials(options) { return { cookies, warnings }; } const sourcesToTry = resolveSources(options.cookieSource); + let getCookies; + try { + ({ getCookies } = await loadSweetCookie()); + } + catch (error) { + if (error?.code !== 'ERR_MODULE_NOT_FOUND' || + !String(error.message ?? '').includes('@steipete/sweet-cookie')) { + throw error; + } + warnings.push('Browser cookie lookup unavailable because vendored dependency @steipete/sweet-cookie is not installed.'); + if (!cookies.authToken) { + warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox'); + } + if (!cookies.ct0) { + warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox'); + } + return { cookies, warnings }; + } for (const source of sourcesToTry) { const res = await readTwitterCookiesFromBrowser({ + getCookies, source, chromeProfile: options.chromeProfile, firefoxProfile: options.firefoxProfile, diff --git a/tests/test_bird_x.py b/tests/test_bird_x.py index b50c9b6..20ad216 100644 --- a/tests/test_bird_x.py +++ b/tests/test_bird_x.py @@ -1,3 +1,7 @@ +import json +import os +import shutil +import subprocess import sys import unittest from pathlib import Path @@ -7,6 +11,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from lib.bird_x import parse_bird_response +REPO_ROOT = Path(__file__).resolve().parents[1] +VENDORED_BIRD = REPO_ROOT / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs" + + class TestBirdXEngagementZero(unittest.TestCase): def test_zero_likes_preserved(self): tweets = [ @@ -22,6 +30,49 @@ class TestBirdXEngagementZero(unittest.TestCase): self.assertEqual(0, items[0]["engagement"]["likes"]) self.assertEqual(5, items[0]["engagement"]["reposts"]) + +@unittest.skipUnless(shutil.which("node"), "node is required for vendored Bird tests") +class TestVendoredBirdRuntime(unittest.TestCase): + def test_check_uses_env_credentials_without_browser_cookie_dependency(self): + env = os.environ.copy() + env["AUTH_TOKEN"] = "dummy-auth" + env["CT0"] = "dummy-ct0" + + result = subprocess.run( + ["node", str(VENDORED_BIRD), "--check"], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(0, result.returncode, result.stderr) + payload = json.loads(result.stdout) + self.assertTrue(payload["authenticated"]) + self.assertEqual("env AUTH_TOKEN", payload["source"]) + + def test_check_with_browser_lookup_disabled_returns_json_warnings(self): + env = os.environ.copy() + env.pop("AUTH_TOKEN", None) + env.pop("CT0", None) + env["BIRD_DISABLE_BROWSER_COOKIES"] = "1" + + result = subprocess.run( + ["node", str(VENDORED_BIRD), "--check"], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(1, result.returncode, result.stderr) + payload = json.loads(result.stdout) + self.assertFalse(payload["authenticated"]) + self.assertTrue(payload["warnings"]) + self.assertIn("Missing auth_token", " ".join(payload["warnings"])) + def test_none_likes_when_missing(self): tweets = [ { From b7d1a38ff6b09f9307d3e7cce47e280de5b2a1b5 Mon Sep 17 00:00:00 2001 From: Pejman Pour-Moezzi Date: Wed, 8 Apr 2026 19:30:39 -0700 Subject: [PATCH 2/2] Fix Bird cookie helper lazy-loading --- scripts/lib/vendor/bird-search/lib/cookies.js | 13 ++- tests/test_bird_x.py | 92 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/scripts/lib/vendor/bird-search/lib/cookies.js b/scripts/lib/vendor/bird-search/lib/cookies.js index 636fc82..eb0d85b 100644 --- a/scripts/lib/vendor/bird-search/lib/cookies.js +++ b/scripts/lib/vendor/bird-search/lib/cookies.js @@ -117,14 +117,21 @@ async function readTwitterCookiesFromBrowser(options) { } return { cookies: out, warnings }; } +async function extractCookiesFromBrowser(options) { + const { getCookies } = await loadSweetCookie(); + return readTwitterCookiesFromBrowser({ + getCookies, + ...options, + }); +} export async function extractCookiesFromSafari() { - return readTwitterCookiesFromBrowser({ source: 'safari' }); + return extractCookiesFromBrowser({ source: 'safari' }); } export async function extractCookiesFromChrome(profile) { - return readTwitterCookiesFromBrowser({ source: 'chrome', chromeProfile: profile }); + return extractCookiesFromBrowser({ source: 'chrome', chromeProfile: profile }); } export async function extractCookiesFromFirefox(profile) { - return readTwitterCookiesFromBrowser({ source: 'firefox', firefoxProfile: profile }); + return extractCookiesFromBrowser({ source: 'firefox', firefoxProfile: profile }); } /** * Resolve Twitter credentials from multiple sources. diff --git a/tests/test_bird_x.py b/tests/test_bird_x.py index 20ad216..f85708c 100644 --- a/tests/test_bird_x.py +++ b/tests/test_bird_x.py @@ -3,6 +3,7 @@ import os import shutil import subprocess import sys +import textwrap import unittest from pathlib import Path @@ -73,6 +74,97 @@ class TestVendoredBirdRuntime(unittest.TestCase): self.assertTrue(payload["warnings"]) self.assertIn("Missing auth_token", " ".join(payload["warnings"])) + def test_browser_cookie_helpers_lazy_load_sweet_cookie(self): + sweet_cookie_dir = ( + REPO_ROOT + / "scripts" + / "lib" + / "vendor" + / "bird-search" + / "lib" + / "node_modules" + / "@steipete" + / "sweet-cookie" + ) + if sweet_cookie_dir.exists(): + self.skipTest("vendored sweet-cookie test stub already exists") + + sweet_cookie_dir.mkdir(parents=True) + (sweet_cookie_dir / "package.json").write_text( + json.dumps( + { + "name": "@steipete/sweet-cookie", + "type": "module", + "exports": "./index.js", + } + ), + encoding="utf-8", + ) + (sweet_cookie_dir / "index.js").write_text( + textwrap.dedent( + """ + export async function getCookies(options) { + const browser = options.browsers?.[0] ?? "unknown"; + return { + cookies: [ + { name: "auth_token", value: `${browser}-auth`, domain: "x.com" }, + { name: "ct0", value: `${browser}-ct0`, domain: "x.com" }, + ], + warnings: [], + }; + } + """ + ), + encoding="utf-8", + ) + + try: + result = subprocess.run( + [ + "node", + "--input-type=module", + "-e", + textwrap.dedent( + """ + import { + extractCookiesFromSafari, + extractCookiesFromChrome, + extractCookiesFromFirefox, + } from "./scripts/lib/vendor/bird-search/lib/cookies.js"; + + const payload = await Promise.all([ + extractCookiesFromSafari(), + extractCookiesFromChrome("Profile 1"), + extractCookiesFromFirefox("default-release"), + ]); + process.stdout.write(JSON.stringify(payload)); + """ + ), + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(0, result.returncode, result.stderr) + payload = json.loads(result.stdout) + self.assertEqual("Safari", payload[0]["cookies"]["source"]) + self.assertEqual('Chrome profile "Profile 1"', payload[1]["cookies"]["source"]) + self.assertEqual( + 'Firefox profile "default-release"', payload[2]["cookies"]["source"] + ) + self.assertEqual("safari-auth", payload[0]["cookies"]["authToken"]) + self.assertEqual("chrome-auth", payload[1]["cookies"]["authToken"]) + self.assertEqual("firefox-auth", payload[2]["cookies"]["authToken"]) + finally: + shutil.rmtree(sweet_cookie_dir, ignore_errors=True) + for path in [sweet_cookie_dir.parent, sweet_cookie_dir.parent.parent]: + try: + path.rmdir() + except OSError: + pass + def test_none_likes_when_missing(self): tweets = [ {