chore(lint): enable obsidianmd/no-global-this and fix violations (#2413)

Removes the deferred `"off"` override and rewrites the 55 violations
(all in test/mocks/Node-script files) to use `window.*` instead of
`global.*`. Production code was already cleaned up in #2401.

- 18 test files + __mocks__/obsidian.js + jest.setup.js: mechanical
  `global.X` → `window.X` (Jest runs under jsdom, so window === globalThis)
- scripts/printPromptDebugEntry.ts: inline eslint-disable — Node-only
  debug script with no window available

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zero Liu 2026-05-13 01:14:47 -07:00 committed by GitHub
parent 48794ce097
commit 710acc2436
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 55 additions and 57 deletions

View file

@ -82,7 +82,7 @@ module.exports = {
};
// Mock the global app object
global.app = {
window.app = {
vault: {
getAbstractFileByPath: jest.fn().mockReturnValue({
name: "test-file.md",

View file

@ -103,9 +103,6 @@ export default [
// no-deprecated: defer — surface the warnings, but don't fail CI yet
"@typescript-eslint/no-deprecated": "off",
// obsidianmd/no-global-this fires on jest globalThis usage in tests and
// a small number of legit cases — defer triage.
"obsidianmd/no-global-this": "off",
// rule-custom-message wraps no-console; we don't enable no-console in this PR
// and the codebase enforces logInfo/logWarn/logError via CLAUDE.md.
"obsidianmd/rule-custom-message": "off",

View file

@ -1,15 +1,15 @@
import "web-streams-polyfill/dist/polyfill.min.js";
import { TextEncoder, TextDecoder } from "util";
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
window.TextEncoder = TextEncoder;
window.TextDecoder = TextDecoder;
// Polyfill Obsidian's Node.doc / Node.win augmentation so plugin code that
// reads `element.doc` / `element.win` works under jsdom.
if (typeof Node !== "undefined" && !Object.prototype.hasOwnProperty.call(Node.prototype, "doc")) {
Object.defineProperty(Node.prototype, "doc", {
get() {
return this.ownerDocument ?? global.document;
return this.ownerDocument ?? window.document;
},
configurable: true,
});
@ -17,7 +17,7 @@ if (typeof Node !== "undefined" && !Object.prototype.hasOwnProperty.call(Node.pr
if (typeof Node !== "undefined" && !Object.prototype.hasOwnProperty.call(Node.prototype, "win")) {
Object.defineProperty(Node.prototype, "win", {
get() {
return this.ownerDocument?.defaultView ?? global.window;
return this.ownerDocument?.defaultView ?? window;
},
configurable: true,
});

View file

@ -94,6 +94,7 @@ export async function run(args: string[]): Promise<void> {
}
const app = createHeadlessApp();
// eslint-disable-next-line obsidianmd/no-global-this -- node-only debug script, no window available
(global as unknown as { app: unknown }).app = app;
initializeBuiltinTools();

View file

@ -9,7 +9,7 @@ describe("Image extraction from content", () => {
},
};
(global as any).app = mockApp;
(window as any).app = mockApp;
beforeEach(() => {
jest.clearAllMocks();

View file

@ -19,7 +19,7 @@ jest.mock("@/logger", () => ({
// Mock global fetch
const mockFetch = jest.fn();
global.fetch = mockFetch;
window.fetch = mockFetch;
beforeEach(() => {
jest.clearAllMocks();

View file

@ -670,7 +670,7 @@ describe("parseCustomCommandFile", () => {
beforeEach(() => {
// Save and mock global app
originalApp = global.app;
originalApp = window.app;
mockFrontmatter = {
"copilot-command-context-menu-enabled": true,
"copilot-command-slash-enabled": false,
@ -679,7 +679,7 @@ describe("parseCustomCommandFile", () => {
"copilot-command-last-used": 1234567890,
};
mockMetadata = { frontmatter: mockFrontmatter };
global.app = {
window.app = {
vault: {
read: jest
.fn()
@ -699,7 +699,7 @@ describe("parseCustomCommandFile", () => {
});
afterEach(() => {
global.app = originalApp;
window.app = originalApp;
});
it("parses a custom command file with frontmatter and content", async () => {
@ -719,8 +719,8 @@ describe("parseCustomCommandFile", () => {
});
it("uses EMPTY_COMMAND defaults if frontmatter is missing", async () => {
(global.app.vault as any).read.mockResolvedValue("Prompt content only, no frontmatter.");
(global.app.metadataCache as any).getFileCache.mockReturnValue({});
(window.app.vault as any).read.mockResolvedValue("Prompt content only, no frontmatter.");
(window.app.metadataCache as any).getFileCache.mockReturnValue({});
const { parseCustomCommandFile } = await import("@/commands/customCommandUtils");
const result = await parseCustomCommandFile(mockFile);
expect(result).toEqual({

View file

@ -27,7 +27,7 @@ const mockApp = {
};
// Mock global app
Object.defineProperty(global, "app", {
Object.defineProperty(window, "app", {
value: mockApp,
writable: true,
});

View file

@ -12,7 +12,7 @@ import { ContextProcessor } from "@/contextProcessor";
import { DATAVIEW_BLOCK_TAG } from "@/constants";
// Mock the global app object for Dataview plugin access
global.app = {
window.app = {
plugins: {
plugins: {},
},
@ -25,7 +25,7 @@ describe("ContextProcessor - Dataview Integration", () => {
beforeEach(() => {
contextProcessor = ContextProcessor.getInstance();
// Reset plugins for each test
(global.app as any).plugins.plugins = {};
(window.app as any).plugins.plugins = {};
// Save and mock console.error to suppress expected error messages
originalConsoleError = console.error;
console.error = jest.fn();
@ -44,7 +44,7 @@ describe("ContextProcessor - Dataview Integration", () => {
});
it("should return content unchanged when Dataview API is not available", async () => {
(global.app as any).plugins.plugins.dataview = {};
(window.app as any).plugins.plugins.dataview = {};
const content = "```dataview\nLIST\n```";
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
expect(result).toBe(content);
@ -54,7 +54,7 @@ describe("ContextProcessor - Dataview Integration", () => {
describe("processDataviewBlocks - Regex Pattern Matching", () => {
beforeEach(() => {
// Mock successful Dataview plugin with API
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -105,7 +105,7 @@ describe("ContextProcessor - Dataview Integration", () => {
describe("processDataviewBlocks - Multiple Blocks", () => {
beforeEach(() => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest
.fn()
@ -172,7 +172,7 @@ LIST
describe("processDataviewBlocks - Query Execution", () => {
it("should include both original query and executed results", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -194,7 +194,7 @@ LIST
});
it("should handle query timeout gracefully", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockImplementation(
() =>
@ -213,7 +213,7 @@ LIST
}, 10000); // Increase test timeout to 10s
it("should handle query execution errors", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: false,
@ -229,7 +229,7 @@ LIST
});
it("should handle dataviewjs with unsupported message", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn(),
},
@ -244,7 +244,7 @@ LIST
describe("processDataviewBlocks - Result Formatting", () => {
it("should format LIST results correctly", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -265,7 +265,7 @@ LIST
});
it("should format TABLE results correctly", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -291,7 +291,7 @@ LIST
});
it("should format TASK results correctly", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -314,7 +314,7 @@ LIST
});
it("should handle empty results", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -333,7 +333,7 @@ LIST
});
it("should handle null values gracefully", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -354,7 +354,7 @@ LIST
});
it("should handle arrays in values", async () => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,
@ -375,7 +375,7 @@ LIST
describe("processDataviewBlocks - Edge Cases", () => {
beforeEach(() => {
(global.app as any).plugins.plugins.dataview = {
(window.app as any).plugins.plugins.dataview = {
api: {
query: jest.fn().mockResolvedValue({
successful: true,

View file

@ -39,7 +39,7 @@ describe("ContextProcessor - Embedded Notes", () => {
getFileCache: jest.fn((file: TFile) => fileCaches[file.path] ?? {}),
};
(global as any).app = {
(window as any).app = {
metadataCache: metadataCacheMock,
};

View file

@ -15,8 +15,8 @@ const mockElectron = {
jest.mock("electron", () => mockElectron);
global.TextEncoder = TextEncoder as any;
global.TextDecoder = TextDecoder as any;
window.TextEncoder = TextEncoder as any;
window.TextDecoder = TextDecoder as any;
// Now we can import our modules
import { encryptAllKeys, getDecryptedKey, getEncryptedKey } from "@/encryptionService";
@ -25,8 +25,8 @@ import { Platform } from "obsidian";
import { Buffer } from "buffer";
// Mock window.btoa and window.atob for base64 encoding/decoding
global.btoa = jest.fn().mockImplementation((str) => Buffer.from(str).toString("base64"));
global.atob = jest.fn().mockImplementation((str) => Buffer.from(str, "base64").toString());
window.btoa = jest.fn().mockImplementation((str) => Buffer.from(str).toString("base64"));
window.atob = jest.fn().mockImplementation((str) => Buffer.from(str, "base64").toString());
const mockSubtle = {
importKey: jest.fn().mockResolvedValue("mockCryptoKey"),
@ -43,7 +43,7 @@ const mockSubtle = {
};
// Mock crypto.subtle instead of the entire crypto object
Object.defineProperty(global.crypto, "subtle", {
Object.defineProperty(window.crypto, "subtle", {
value: mockSubtle,
configurable: true,
});

View file

@ -35,7 +35,7 @@ function makeMockFile(path: string): TFile {
// Helper: set up the global `app` mock used by parseProjectConfigFile
function setupAppMock(rawContent: string, frontmatter: Record<string, unknown> | null) {
(global as Record<string, unknown>).app = {
(window as unknown as Record<string, unknown>).app = {
vault: {
read: jest.fn().mockResolvedValue(rawContent),
// Reason: parseProjectConfigFile uses `cachedFile instanceof TFile` to detect synthetic TFiles.

View file

@ -133,7 +133,7 @@ describe("findRelevantNotes", () => {
["linked-only.md", linkedOnly],
]);
(global.app.vault.getAbstractFileByPath as jest.Mock).mockImplementation((path: string) => {
(window.app.vault.getAbstractFileByPath as jest.Mock).mockImplementation((path: string) => {
return filesByPath.get(path) ?? null;
});

View file

@ -68,12 +68,12 @@ jest.mock("@/settings/model", () => ({
describe("searchUtils", () => {
beforeAll(() => {
// @ts-ignore
global.app = mockApp;
window.app = mockApp;
});
afterAll(() => {
// @ts-ignore
delete global.app;
delete window.app;
});
beforeEach(() => {

View file

@ -75,14 +75,14 @@ describe("migrateSystemPromptsFromSettings", () => {
} as unknown as Vault;
// Mock global app
originalApp = global.app;
global.app = {
originalApp = window.app;
window.app = {
vault: mockVault,
} as any;
});
afterEach(() => {
global.app = originalApp;
window.app = originalApp;
});
it("skips migration when userSystemPrompt is empty", async () => {

View file

@ -66,8 +66,8 @@ describe("SystemPromptManager", () => {
} as unknown as Vault;
// Mock global app
originalApp = global.app;
global.app = {
originalApp = window.app;
window.app = {
vault: mockVault,
fileManager: {
processFrontMatter: jest.fn(),
@ -81,7 +81,7 @@ describe("SystemPromptManager", () => {
});
afterEach(() => {
global.app = originalApp;
window.app = originalApp;
});
describe("getInstance", () => {

View file

@ -250,14 +250,14 @@ describe("parseSystemPromptFile", () => {
let mockFile: TFile;
beforeEach(() => {
originalApp = global.app;
originalApp = window.app;
mockFile = mockTFile({
basename: "Test Prompt",
path: "SystemPrompts/Test Prompt.md",
extension: "md",
});
global.app = {
window.app = {
vault: {
read: jest.fn(),
},
@ -268,7 +268,7 @@ describe("parseSystemPromptFile", () => {
});
afterEach(() => {
global.app = originalApp;
window.app = originalApp;
});
it("parses a file with frontmatter and content", async () => {

View file

@ -127,7 +127,7 @@ describe("ProjectContextCache", () => {
jest.clearAllMocks();
// Mock globals
global.app = mockApp as any;
window.app = mockApp as any;
// Set up mock vault file retrieval
mockApp.vault.getFiles.mockReturnValue([mockMarkdownFile, mockPdfFile]);

View file

@ -35,14 +35,14 @@ describe("readNoteTool", () => {
beforeEach(async () => {
jest.resetModules();
originalApp = global.app;
originalApp = window.app;
getAbstractFileByPathMock = jest.fn();
getMarkdownFilesMock = jest.fn().mockReturnValue([]);
getFirstLinkpathDestMock = jest.fn().mockReturnValue(null);
mockRead.mockReset();
mockRead.mockResolvedValue("");
global.app = {
window.app = {
vault: {
getAbstractFileByPath: getAbstractFileByPathMock,
getMarkdownFiles: getMarkdownFilesMock,
@ -62,7 +62,7 @@ describe("readNoteTool", () => {
});
afterEach(() => {
global.app = originalApp;
window.app = originalApp;
});
it("returns the first chunk with follow-up metadata", async () => {

View file

@ -265,7 +265,7 @@ describe("getNotesFromPath", () => {
describe("getNotesFromTags", () => {
beforeAll(() => {
// @ts-ignore
global.app = mockApp;
window.app = mockApp;
// Setup metadata cache mock
mockMetadataCache.getFileCache.mockImplementation((file: TFile) => {
@ -275,7 +275,7 @@ describe("getNotesFromTags", () => {
afterAll(() => {
// @ts-ignore
delete global.app;
delete window.app;
});
beforeEach(() => {