3301 lines · 79.8 KB · TypeScript
RawDownload
// @vitest-environment nodeimport { readFileSync } from "node:fs";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const ensureBirdclawDirsMock = vi.fn();const getBirdclawPathsMock = vi.fn();const getDefaultAccountSelectorMock = vi.fn();const resolveOperationAccountMock = vi.fn();const resolveMentionsDataSourceMock = vi.fn();const setActionsTransportMock = vi.fn();const getQueryEnvelopeMock = vi.fn();const getNativeDbMock = vi.fn();const seedDemoDataMock = vi.fn();const findArchivesMock = vi.fn();const importArchiveMock = vi.fn();const importTweetsViaFxTwitterMock = vi.fn();const importThreadViaFxTwitterMock = vi.fn();const importConversationViaFxTwitterMock = vi.fn();const importProfileViaFxTwitterMock = vi.fn();const importSearchViaFxTwitterMock = vi.fn();const importBlocklistMock = vi.fn();const addBlockMock = vi.fn();const recordBlockMock = vi.fn();const syncBlocksMock = vi.fn();const exportMentionItemsMock = vi.fn();const exportMentionsViaCachedAutoMock = vi.fn();const exportMentionsViaCachedBirdMock = vi.fn();const exportMentionsViaCachedXurlMock = vi.fn();const syncMentionsMock = vi.fn();const syncMentionThreadsMock = vi.fn();const syncDirectMessagesViaCachedBirdMock = vi.fn();const resolveProfilesForIdsMock = vi.fn();const expandUrlsFromTextsMock = vi.fn();const searchLinksMock = vi.fn();const backfillLinkIndexMock = vi.fn();const fetchTweetMediaMock = vi.fn();const formatMediaFetchResultMock = vi.fn();const runWhoisMock = vi.fn();const formatWhoisMock = vi.fn();const listBlocksMock = vi.fn();const addMuteMock = vi.fn();const listMutesMock = vi.fn();const recordMuteMock = vi.fn();const listInboxItemsMock = vi.fn();const scoreInboxMock = vi.fn();const syncFollowGraphMock = vi.fn();const getFollowGraphSummaryMock = vi.fn();const listFollowEventsMock = vi.fn();const listMutualsMock = vi.fn();const listNonMutualFollowingMock = vi.fn();const listTopFollowersMock = vi.fn();const listUnfollowedSinceMock = vi.fn();const listTimelineItemsMock = vi.fn();const listDmConversationsMock = vi.fn();const applyDmRequestMutationToLocalStoreMock = vi.fn();const runDirectMessageRequestMutationViaBirdMock = vi.fn();const getAuthenticatedBirdAccountMock = vi.fn();const getConversationThreadMock = vi.fn();const hydrateProfilesFromXMock = vi.fn();const inspectProfileRepliesMock = vi.fn();const runResearchModeMock = vi.fn();const streamPeriodDigestMock = vi.fn();const streamSearchDiscussionMock = vi.fn();const syncAuthoredTweetsMock = vi.fn();const syncTimelineCollectionMock = vi.fn();const createPostMock = vi.fn();const createTweetReplyMock = vi.fn();const createDmReplyMock = vi.fn();const removeBlockMock = vi.fn();const removeMuteMock = vi.fn();const maybeAutoUpdateBackupMock = vi.fn();const maybeAutoSyncBackupMock = vi.fn();const exportBackupMock = vi.fn();const importBackupMock = vi.fn();const syncBackupMock = vi.fn();const validateBackupMock = vi.fn();const runProductionServerMock = vi.fn();const packageVersion = (	JSON.parse(		readFileSync(new URL("../package.json", import.meta.url), "utf8"),	) as { version: string }).version;const execFileAsyncMock = vi.fn();const execFileMock = vi.fn();const consoleLogMock = vi.spyOn(console, "log").mockImplementation(() => {}); class FxTwitterErrorMock extends Error {	readonly kind: string;	readonly status?: number;	readonly retryAfterMs?: number; 	constructor(options: {		kind: string;		message: string;		status?: number;		retryAfterMs?: number;	}) {		super(options.message);		this.kind = options.kind;		this.status = options.status;		this.retryAfterMs = options.retryAfterMs;	}} Object.defineProperty(	execFileMock,	Symbol.for("nodejs.util.promisify.custom"),	{		value: execFileAsyncMock,	},); vi.mock("#/lib/config", () => ({	ensureBirdclawDirs: () => ensureBirdclawDirsMock(),	getDefaultAccountSelector: () => getDefaultAccountSelectorMock(),	getBirdclawConfig: () => ({}),	getBirdclawPaths: () => getBirdclawPathsMock(),	resolveMentionsDataSource: (...args: unknown[]) =>		resolveMentionsDataSourceMock(...args),	setActionsTransport: (...args: unknown[]) => setActionsTransportMock(...args),})); vi.mock("#/lib/db", () => ({	closeDatabase: vi.fn(),	getNativeDb: (...args: unknown[]) => getNativeDbMock(...args),})); vi.mock("#/lib/seed", () => ({	seedDemoData: (...args: unknown[]) => seedDemoDataMock(...args),})); vi.mock("#/lib/account-selection", () => ({	resolveOperationAccount: (...args: unknown[]) =>		resolveOperationAccountMock(...args),})); vi.mock("#/lib/dm-read-model", () => ({	getConversationThread: (...args: unknown[]) =>		getConversationThreadMock(...args),})); vi.mock("#/lib/archive-finder", () => ({	findArchives: () => findArchivesMock(),})); vi.mock("#/lib/archive-import", () => ({	ARCHIVE_IMPORT_SLICES: [		"tweets",		"likes",		"bookmarks",		"directMessages",		"profiles",		"followers",		"following",	],	importArchive: (...args: unknown[]) => importArchiveMock(...args),})); vi.mock("#/lib/fxtwitter", () => ({	FxTwitterError: FxTwitterErrorMock,	importConversationViaFxTwitter: (...args: unknown[]) =>		importConversationViaFxTwitterMock(...args),	importProfileViaFxTwitter: (...args: unknown[]) =>		importProfileViaFxTwitterMock(...args),	importSearchViaFxTwitter: (...args: unknown[]) =>		importSearchViaFxTwitterMock(...args),	importThreadViaFxTwitter: (...args: unknown[]) =>		importThreadViaFxTwitterMock(...args),	importTweetsViaFxTwitter: (...args: unknown[]) =>		importTweetsViaFxTwitterMock(...args),})); vi.mock("#/lib/backup", async (importOriginal) => {	const actual = await importOriginal<typeof import("#/lib/backup")>();	return {		...actual,		exportBackup: (...args: unknown[]) => exportBackupMock(...args),		importBackup: (...args: unknown[]) => importBackupMock(...args),		maybeAutoUpdateBackup: () => maybeAutoUpdateBackupMock(),		maybeAutoSyncBackup: () => maybeAutoSyncBackupMock(),		syncBackup: (...args: unknown[]) => syncBackupMock(...args),		validateBackup: (...args: unknown[]) => validateBackupMock(...args),	};}); vi.mock("#/lib/blocklist", () => ({	importBlocklist: (...args: unknown[]) => importBlocklistMock(...args),})); vi.mock("#/lib/blocks", () => ({	addBlock: (...args: unknown[]) => addBlockMock(...args),	listBlocks: (...args: unknown[]) => listBlocksMock(...args),	recordBlock: (...args: unknown[]) => recordBlockMock(...args),	removeBlock: (...args: unknown[]) => removeBlockMock(...args),	syncBlocks: (...args: unknown[]) => syncBlocksMock(...args),})); vi.mock("#/lib/inbox", () => ({	listInboxItems: (...args: unknown[]) => listInboxItemsMock(...args),	scoreInbox: (...args: unknown[]) => scoreInboxMock(...args),})); vi.mock("#/lib/link-index", () => ({	backfillLinkIndex: (...args: unknown[]) => backfillLinkIndexMock(...args),	searchLinks: (...args: unknown[]) => searchLinksMock(...args),})); vi.mock("#/lib/media-fetch", () => ({	fetchTweetMedia: (...args: unknown[]) => fetchTweetMediaMock(...args),	formatMediaFetchResult: (...args: unknown[]) =>		formatMediaFetchResultMock(...args),})); vi.mock("#/lib/follow-graph", () => ({	getFollowGraphSummary: (...args: unknown[]) =>		getFollowGraphSummaryMock(...args),	listFollowEvents: (...args: unknown[]) => listFollowEventsMock(...args),	listMutuals: (...args: unknown[]) => listMutualsMock(...args),	listNonMutualFollowing: (...args: unknown[]) =>		listNonMutualFollowingMock(...args),	listTopFollowers: (...args: unknown[]) => listTopFollowersMock(...args),	listUnfollowedSince: (...args: unknown[]) => listUnfollowedSinceMock(...args),	syncFollowGraph: (...args: unknown[]) => syncFollowGraphMock(...args),})); vi.mock("#/lib/mutes", () => ({	addMute: (...args: unknown[]) => addMuteMock(...args),	listMutes: (...args: unknown[]) => listMutesMock(...args),	recordMute: (...args: unknown[]) => recordMuteMock(...args),	removeMute: (...args: unknown[]) => removeMuteMock(...args),})); vi.mock("#/lib/mentions-export", () => ({	exportMentionItems: (...args: unknown[]) => exportMentionItemsMock(...args),})); vi.mock("#/lib/mentions-live", () => ({	exportMentionsViaCachedAuto: (...args: unknown[]) =>		exportMentionsViaCachedAutoMock(...args),	exportMentionsViaCachedBird: (...args: unknown[]) =>		exportMentionsViaCachedBirdMock(...args),	exportMentionsViaCachedXurl: (...args: unknown[]) =>		exportMentionsViaCachedXurlMock(...args),	syncMentions: (...args: unknown[]) => syncMentionsMock(...args),})); vi.mock("#/lib/mention-threads-live", () => ({	syncMentionThreads: (...args: unknown[]) => syncMentionThreadsMock(...args),})); vi.mock("#/lib/dms-live", () => ({	syncDirectMessagesViaCachedBird: (...args: unknown[]) =>		syncDirectMessagesViaCachedBirdMock(...args),})); vi.mock("#/lib/profile-hydration", () => ({	hydrateProfilesFromX: (...args: unknown[]) =>		hydrateProfilesFromXMock(...args),})); vi.mock("#/lib/profile-resolver", () => ({	resolveProfilesForIds: (...args: unknown[]) =>		resolveProfilesForIdsMock(...args),})); vi.mock("#/lib/profile-replies", () => ({	inspectProfileReplies: (...args: unknown[]) =>		inspectProfileRepliesMock(...args),})); vi.mock("#/lib/research", () => ({	runResearchMode: (...args: unknown[]) => runResearchModeMock(...args),})); vi.mock("#/lib/period-digest", () => ({	normalizeDigestLanguage: (value: string | undefined) => {		const trimmed = value?.trim();		if (!trimmed) return undefined;		if (trimmed === "ZH-cn") return "zh-CN";		throw new Error(			"Digest language must be a valid Unicode locale identifier",		);	},	streamPeriodDigest: (...args: unknown[]) => streamPeriodDigestMock(...args),})); vi.mock("#/lib/search-discussion", () => ({	streamSearchDiscussion: (...args: unknown[]) =>		streamSearchDiscussionMock(...args),})); vi.mock("#/lib/authored-live", () => ({	AuthoredSyncError: class AuthoredSyncError extends Error {		constructor(			message: string,			public readonly exitCode: number,		) {			super(message);		}	},	syncAuthoredTweets: (...args: unknown[]) => syncAuthoredTweetsMock(...args),})); vi.mock("#/lib/queries", () => ({	applyDmRequestMutationToLocalStore: (...args: unknown[]) =>		applyDmRequestMutationToLocalStoreMock(...args),	getQueryEnvelope: (...args: unknown[]) => getQueryEnvelopeMock(...args),	listTimelineItems: (...args: unknown[]) => listTimelineItemsMock(...args),	listDmConversations: (...args: unknown[]) => listDmConversationsMock(...args),	createPost: (...args: unknown[]) => createPostMock(...args),	createTweetReply: (...args: unknown[]) => createTweetReplyMock(...args),	createDmReply: (...args: unknown[]) => createDmReplyMock(...args),})); vi.mock("#/lib/production-server", () => ({	runProductionServer: (...args: unknown[]) => runProductionServerMock(...args),})); vi.mock("#/lib/bird", () => ({	getAuthenticatedBirdAccount: (...args: unknown[]) =>		getAuthenticatedBirdAccountMock(...args),	runDirectMessageRequestMutationViaBird: (...args: unknown[]) =>		runDirectMessageRequestMutationViaBirdMock(...args),})); vi.mock("#/lib/timeline-collections-live", () => ({	syncTimelineCollection: (...args: unknown[]) =>		syncTimelineCollectionMock(...args),})); vi.mock("#/lib/url-expansion", () => ({	expandUrlsFromTexts: (...args: unknown[]) => expandUrlsFromTextsMock(...args),})); vi.mock("#/lib/whois", () => ({	formatWhois: (...args: unknown[]) => formatWhoisMock(...args),	runWhois: (...args: unknown[]) => runWhoisMock(...args),})); vi.mock("node:child_process", () => ({	execFile: execFileMock,})); async function loadCli() {	vi.resetModules();	return import("./cli");} describe("cli", () => {	beforeEach(() => {		process.exitCode = undefined;		consoleLogMock.mockClear();		ensureBirdclawDirsMock.mockReset();		getBirdclawPathsMock.mockReset();		getDefaultAccountSelectorMock.mockReset();		resolveOperationAccountMock.mockReset();		resolveMentionsDataSourceMock.mockReset();		setActionsTransportMock.mockReset();		getQueryEnvelopeMock.mockReset();		getNativeDbMock.mockReset();		seedDemoDataMock.mockReset();		findArchivesMock.mockReset();		importArchiveMock.mockReset();		importTweetsViaFxTwitterMock.mockReset();		importThreadViaFxTwitterMock.mockReset();		importConversationViaFxTwitterMock.mockReset();		importProfileViaFxTwitterMock.mockReset();		importSearchViaFxTwitterMock.mockReset();		importBlocklistMock.mockReset();		addBlockMock.mockReset();		recordBlockMock.mockReset();		syncBlocksMock.mockReset();		exportMentionItemsMock.mockReset();		exportMentionsViaCachedAutoMock.mockReset();		exportMentionsViaCachedBirdMock.mockReset();		exportMentionsViaCachedXurlMock.mockReset();		syncMentionsMock.mockReset();		syncMentionThreadsMock.mockReset();		syncDirectMessagesViaCachedBirdMock.mockReset();		resolveProfilesForIdsMock.mockReset();		expandUrlsFromTextsMock.mockReset();		searchLinksMock.mockReset();		backfillLinkIndexMock.mockReset();		fetchTweetMediaMock.mockReset();		formatMediaFetchResultMock.mockReset();		runWhoisMock.mockReset();		formatWhoisMock.mockReset();		listBlocksMock.mockReset();		addMuteMock.mockReset();		listMutesMock.mockReset();		recordMuteMock.mockReset();		listInboxItemsMock.mockReset();		scoreInboxMock.mockReset();		syncFollowGraphMock.mockReset();		getFollowGraphSummaryMock.mockReset();		listFollowEventsMock.mockReset();		listMutualsMock.mockReset();		listNonMutualFollowingMock.mockReset();		listTopFollowersMock.mockReset();		listUnfollowedSinceMock.mockReset();		listTimelineItemsMock.mockReset();		listDmConversationsMock.mockReset();		applyDmRequestMutationToLocalStoreMock.mockReset();		runDirectMessageRequestMutationViaBirdMock.mockReset();		getAuthenticatedBirdAccountMock.mockReset();		getConversationThreadMock.mockReset();		hydrateProfilesFromXMock.mockReset();		inspectProfileRepliesMock.mockReset();		runResearchModeMock.mockReset();		streamPeriodDigestMock.mockReset();		streamSearchDiscussionMock.mockReset();		syncAuthoredTweetsMock.mockReset();		syncTimelineCollectionMock.mockReset();		createPostMock.mockReset();		createTweetReplyMock.mockReset();		createDmReplyMock.mockReset();		removeBlockMock.mockReset();		removeMuteMock.mockReset();		maybeAutoUpdateBackupMock.mockReset();		maybeAutoSyncBackupMock.mockReset();		exportBackupMock.mockReset();		importBackupMock.mockReset();		syncBackupMock.mockReset();		validateBackupMock.mockReset();		runProductionServerMock.mockReset();		execFileAsyncMock.mockReset(); 		ensureBirdclawDirsMock.mockReturnValue({			rootDir: "/tmp/.birdclaw",			configPath: "/tmp/.birdclaw/config.json",			dbPath: "/tmp/.birdclaw/birdclaw.sqlite",			mediaOriginalsDir: "/tmp/.birdclaw/media/originals",			mediaThumbsDir: "/tmp/.birdclaw/media/thumbs",		});		getBirdclawPathsMock.mockReturnValue({			rootDir: "/tmp/.birdclaw",			dbPath: "/tmp/.birdclaw/birdclaw.sqlite",		});		getDefaultAccountSelectorMock.mockReturnValue(undefined);		resolveOperationAccountMock.mockImplementation((selector?: string) => ({			id:				selector === "steipete" || selector === "@steipete"					? "acct_primary"					: (selector ?? "acct_primary"),			username: selector?.replace(/^@/, "") ?? "steipete",			externalUserId: "25401953",		}));		getConversationThreadMock.mockReturnValue({			conversation: { accountId: "acct_primary" },		});		getAuthenticatedBirdAccountMock.mockResolvedValue({			id: "25401953",			username: "steipete",		});		resolveMentionsDataSourceMock.mockImplementation(			(mode?: string) => mode ?? "birdclaw",		);		setActionsTransportMock.mockImplementation((transport: string) => ({			configPath: "/tmp/.birdclaw/config.json",			transport,		}));		getQueryEnvelopeMock.mockResolvedValue({			stats: { home: 4, mentions: 2, dms: 4, needsReply: 2, inbox: 4 },			transport: { statusText: "local", installed: false },			accounts: [],			archives: [],		});		getNativeDbMock.mockReturnValue({});		seedDemoDataMock.mockReturnValue({			seeded: true,			counts: {				accounts: 2,				profiles: 6,				tweets: 6,				conversations: 4,				messages: 8,			},		});		findArchivesMock.mockResolvedValue([{ name: "twitter.zip" }]);		importArchiveMock.mockResolvedValue({			ok: true,			archivePath: "/tmp/twitter.zip",		});		importTweetsViaFxTwitterMock.mockResolvedValue({			ok: true,			readOnlyTransport: true,			source: "fxtwitter",			endpoint: "https://api.fxtwitter.com",			requestedCount: 1,			importedCount: 1,			items: [				{					tweetId: "20",					source: "fxtwitter",					sourceUrl: "https://api.fxtwitter.com/2/status/20",				},			],		});		for (const mock of [			importThreadViaFxTwitterMock,

Lines 1–500 of 3301.