mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
chore(test): type test mocks to satisfy @typescript-eslint/no-unsafe-call (#2435)
Replaces `any`-typed mock objects in 26 test files with structured types using `jest.Mock`, casts requireMock results to typed shapes, and introduces `internals()` helpers for private-method access. No production code changes; eliminates 487 of 679 no-unsafe-call violations in preparation for enabling the rule. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7587b4fa4d
commit
1203a4dc84
10 changed files with 95 additions and 39 deletions
|
|
@ -92,7 +92,7 @@ export default [
|
|||
// exemptions"). Remaining source files (≤5 violations each) were fixed in
|
||||
// this PR.
|
||||
"@typescript-eslint/no-unsafe-assignment": "off", // enabled for tests below; follow-up PR for production
|
||||
"@typescript-eslint/no-unsafe-call": "off", // 679 violations
|
||||
"@typescript-eslint/no-unsafe-call": "off", // 107 violations
|
||||
|
||||
// --- Medium: promise / method ergonomics ---
|
||||
// Enabled in the TS-only block below.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { AI_SENDER, USER_SENDER } from "@/constants";
|
||||
import { ChatMessage } from "@/types/message";
|
||||
import { updateChatMemory } from "./chatUtils";
|
||||
import type MemoryManager from "@/LLMProviders/memoryManager";
|
||||
|
||||
jest.mock("@/logger");
|
||||
|
||||
|
|
@ -45,8 +44,8 @@ describe("updateChatMemory with tool call markers", () => {
|
|||
},
|
||||
];
|
||||
|
||||
const memoryManager: any = new MockMemoryManager();
|
||||
await updateChatMemory(messages, memoryManager as MemoryManager);
|
||||
const memoryManager = new MockMemoryManager();
|
||||
await updateChatMemory(messages, memoryManager as never);
|
||||
|
||||
expect(memoryManager.getMemory().saved).toHaveLength(1);
|
||||
expect(memoryManager.getMemory().saved[0].input).toBe("find my piano notes");
|
||||
|
|
|
|||
|
|
@ -736,8 +736,12 @@ describe("parseCustomCommandFile", () => {
|
|||
});
|
||||
|
||||
it("uses EMPTY_COMMAND defaults if frontmatter is missing", async () => {
|
||||
(window.app.vault as any).read.mockResolvedValue("Prompt content only, no frontmatter.");
|
||||
(window.app.metadataCache as any).getFileCache.mockReturnValue({});
|
||||
(window.app.vault as unknown as { read: jest.Mock }).read.mockResolvedValue(
|
||||
"Prompt content only, no frontmatter."
|
||||
);
|
||||
(
|
||||
window.app.metadataCache as unknown as { getFileCache: jest.Mock }
|
||||
).getFileCache.mockReturnValue({});
|
||||
const { parseCustomCommandFile } = await import("@/commands/customCommandUtils");
|
||||
const result = await parseCustomCommandFile(mockFile);
|
||||
expect(result).toEqual({
|
||||
|
|
|
|||
|
|
@ -109,7 +109,9 @@ describe("XML Escaping in processPrompt", () => {
|
|||
});
|
||||
|
||||
(getNotesFromPath as jest.Mock).mockReturnValue([]);
|
||||
jest.requireMock("@/utils").extractTemplateNoteFiles.mockReturnValue([mockNote]);
|
||||
(
|
||||
jest.requireMock("@/utils") as unknown as { extractTemplateNoteFiles: jest.Mock }
|
||||
).extractTemplateNoteFiles.mockReturnValue([mockNote]);
|
||||
(getFileContent as jest.Mock).mockResolvedValue("Content with & and < and >");
|
||||
|
||||
const result = await processPrompt(customPrompt, "", mockVault, mockActiveNote);
|
||||
|
|
@ -133,7 +135,9 @@ describe("XML Escaping in processPrompt", () => {
|
|||
});
|
||||
|
||||
(getNotesFromPath as jest.Mock).mockReturnValue([]);
|
||||
jest.requireMock("@/utils").getNotesFromTags.mockReturnValue([mockNote]);
|
||||
(
|
||||
jest.requireMock("@/utils") as unknown as { getNotesFromTags: jest.Mock }
|
||||
).getNotesFromTags.mockReturnValue([mockNote]);
|
||||
(getFileName as jest.Mock).mockReturnValue(mockNote.basename);
|
||||
(getFileContent as jest.Mock).mockResolvedValue("Content: <tag> & </tag>");
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { parseTextForPills } from "./lexicalTextUtils";
|
|||
import { TFile } from "obsidian";
|
||||
import { mockTFolder } from "@/__tests__/mockObsidian";
|
||||
|
||||
const MockTFile = TFile as unknown as jest.Mock;
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("@/logger", () => ({
|
||||
logInfo: jest.fn(),
|
||||
|
|
@ -68,7 +70,7 @@ describe("parseTextForPills", () => {
|
|||
});
|
||||
|
||||
// Mock TFile constructor to set properties
|
||||
(TFile as any).mockImplementation(function (this: any) {
|
||||
MockTFile.mockImplementation(function (this: any) {
|
||||
this.basename = "Valid Note";
|
||||
this.path = "Valid Note.md";
|
||||
});
|
||||
|
|
@ -277,7 +279,7 @@ describe("parseTextForPills", () => {
|
|||
return null;
|
||||
});
|
||||
|
||||
(TFile as any).mockImplementation(function (this: any) {
|
||||
MockTFile.mockImplementation(function (this: any) {
|
||||
this.basename = "Test Note";
|
||||
this.path = "Test Note.md";
|
||||
});
|
||||
|
|
|
|||
|
|
@ -82,12 +82,28 @@ const createContextResult = (content = "Hello with context") => ({
|
|||
contextEnvelope: undefined,
|
||||
});
|
||||
|
||||
type MockChainManager = {
|
||||
memoryManager: { clearChatMemory: jest.Mock };
|
||||
runChain: jest.Mock;
|
||||
};
|
||||
type MockPlugin = {
|
||||
app: {
|
||||
workspace: { getActiveFile: jest.Mock };
|
||||
vault?: { adapter?: { stat: jest.Mock } };
|
||||
};
|
||||
projectManager: {
|
||||
getCurrentChainManager: jest.Mock;
|
||||
getCurrentProjectId: jest.Mock;
|
||||
getCachedMessages: jest.Mock;
|
||||
};
|
||||
};
|
||||
|
||||
describe("ChatManager", () => {
|
||||
let chatManager: ChatManager;
|
||||
let mockMessageRepo: jest.Mocked<MessageRepository>;
|
||||
let mockChainManager: any;
|
||||
let mockFileParserManager: any;
|
||||
let mockPlugin: any;
|
||||
let mockChainManager: MockChainManager;
|
||||
let mockFileParserManager: object;
|
||||
let mockPlugin: MockPlugin;
|
||||
let mockContextManager: jest.Mocked<ContextManager>;
|
||||
|
||||
// Helper function to create mock messages
|
||||
|
|
@ -148,9 +164,9 @@ describe("ChatManager", () => {
|
|||
|
||||
chatManager = new ChatManager(
|
||||
mockMessageRepo,
|
||||
mockChainManager as ChainManager,
|
||||
mockChainManager as unknown as ChainManager,
|
||||
mockFileParserManager as FileParserManager,
|
||||
mockPlugin as CopilotPlugin
|
||||
mockPlugin as unknown as CopilotPlugin
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -92,9 +92,31 @@ jest.mock("@/utils", () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
type MockApp = {
|
||||
vault: {
|
||||
getAbstractFileByPath: jest.Mock;
|
||||
createFolder: jest.Mock;
|
||||
create: jest.Mock;
|
||||
modify: jest.Mock;
|
||||
read: jest.Mock;
|
||||
getMarkdownFiles: jest.Mock;
|
||||
adapter: {
|
||||
exists: jest.Mock;
|
||||
read: jest.Mock;
|
||||
write: jest.Mock;
|
||||
list: jest.Mock;
|
||||
stat: jest.Mock;
|
||||
};
|
||||
};
|
||||
metadataCache: { getFileCache: jest.Mock };
|
||||
fileManager: { processFrontMatter: jest.Mock };
|
||||
};
|
||||
|
||||
type MockMessageRepo = { getDisplayMessages: jest.Mock };
|
||||
|
||||
describe("ChatPersistenceManager", () => {
|
||||
let mockApp: any;
|
||||
let mockMessageRepo: any;
|
||||
let mockApp: MockApp;
|
||||
let mockMessageRepo: MockMessageRepo;
|
||||
let persistenceManager: ChatPersistenceManager;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -133,8 +155,8 @@ describe("ChatPersistenceManager", () => {
|
|||
|
||||
// Create persistence manager
|
||||
persistenceManager = new ChatPersistenceManager(
|
||||
mockApp as App,
|
||||
mockMessageRepo as MessageRepository
|
||||
mockApp as unknown as App,
|
||||
mockMessageRepo as unknown as MessageRepository
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -393,8 +415,8 @@ Nature's quiet song`);
|
|||
} as unknown as ChainManager;
|
||||
|
||||
persistenceManager = new ChatPersistenceManager(
|
||||
mockApp as App,
|
||||
mockMessageRepo as MessageRepository,
|
||||
mockApp as unknown as App,
|
||||
mockMessageRepo as unknown as MessageRepository,
|
||||
chainManager
|
||||
);
|
||||
const mockFile = mockTFile({
|
||||
|
|
@ -1122,8 +1144,8 @@ ${formattedContent}`;
|
|||
|
||||
// Recreate persistence manager with the updated mock
|
||||
const testPersistenceManager = new ChatPersistenceManager(
|
||||
mockApp as App,
|
||||
mockMessageRepo as MessageRepository
|
||||
mockApp as unknown as App,
|
||||
mockMessageRepo as unknown as MessageRepository
|
||||
);
|
||||
|
||||
const originalMessages: ChatMessage[] = [
|
||||
|
|
|
|||
|
|
@ -27,8 +27,17 @@ jest.mock("@/search/searchUtils", () => ({
|
|||
getMatchingPatterns: jest.fn().mockReturnValue({ inclusions: null, exclusions: null }),
|
||||
}));
|
||||
|
||||
type MockApp = {
|
||||
vault: {
|
||||
getAbstractFileByPath: jest.Mock;
|
||||
cachedRead: jest.Mock;
|
||||
getMarkdownFiles: jest.Mock;
|
||||
};
|
||||
metadataCache: { getFileCache: jest.Mock };
|
||||
};
|
||||
|
||||
describe("FilterRetriever", () => {
|
||||
let mockApp: any;
|
||||
let mockApp: MockApp;
|
||||
|
||||
beforeEach(() => {
|
||||
const obsidianMock = getObsidianMock();
|
||||
|
|
@ -55,7 +64,7 @@ describe("FilterRetriever", () => {
|
|||
mockApp.vault.cachedRead.mockResolvedValue("Note content here");
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue({ tags: [{ tag: "#test" }] });
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: [],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
@ -74,7 +83,7 @@ describe("FilterRetriever", () => {
|
|||
const extractNoteFiles = jest.requireMock("@/utils").extractNoteFiles as jest.Mock;
|
||||
extractNoteFiles.mockReturnValueOnce([]);
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: [],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
@ -101,7 +110,7 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
mockApp.vault.cachedRead.mockResolvedValue("Alpha content");
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: ["#project"],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
@ -131,7 +140,7 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
mockApp.vault.cachedRead.mockResolvedValue("Beta content");
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: ["#project"],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
@ -143,7 +152,7 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
|
||||
it("should return empty when no tag terms present", async () => {
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: ["keyword1", "keyword2"],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
@ -168,7 +177,7 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
mockApp.vault.cachedRead.mockResolvedValue("Content");
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: ["#daily"],
|
||||
maxK: 3,
|
||||
});
|
||||
|
|
@ -193,7 +202,7 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
mockApp.vault.cachedRead.mockResolvedValue("Content");
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: ["#daily"],
|
||||
maxK: 3,
|
||||
returnAll: true,
|
||||
|
|
@ -224,7 +233,7 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
mockApp.vault.cachedRead.mockResolvedValue("Shared content");
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: ["#tag1"],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
@ -252,7 +261,7 @@ describe("FilterRetriever", () => {
|
|||
mockApp.vault.cachedRead.mockResolvedValue("Recent content");
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue({ tags: [] });
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: [],
|
||||
maxK: 30,
|
||||
timeRange: {
|
||||
|
|
@ -289,7 +298,7 @@ describe("FilterRetriever", () => {
|
|||
mockApp.vault.cachedRead.mockResolvedValue("Content");
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue({ tags: [] });
|
||||
|
||||
const retriever = new FilterRetriever(mockApp as App, {
|
||||
const retriever = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: [],
|
||||
maxK: 30,
|
||||
timeRange: {
|
||||
|
|
@ -306,14 +315,14 @@ describe("FilterRetriever", () => {
|
|||
});
|
||||
|
||||
it("should report hasTimeRange correctly", () => {
|
||||
const withTime = new FilterRetriever(mockApp as App, {
|
||||
const withTime = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: [],
|
||||
maxK: 30,
|
||||
timeRange: { startTime: 100, endTime: 200 },
|
||||
});
|
||||
expect(withTime.hasTimeRange()).toBe(true);
|
||||
|
||||
const withoutTime = new FilterRetriever(mockApp as App, {
|
||||
const withoutTime = new FilterRetriever(mockApp as unknown as App, {
|
||||
salientTerms: [],
|
||||
maxK: 30,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ jest.mock("@/cache/fileCache", () => {
|
|||
.mockImplementation(
|
||||
(file, additionalContext) => `key-${file.path}-${additionalContext || ""}`
|
||||
),
|
||||
get: jest.fn().mockImplementation(async (key) => {
|
||||
get: jest.fn().mockImplementation(async (key: string) => {
|
||||
// Return mock content based on key
|
||||
if (key.includes("pdf")) return "Mock PDF content";
|
||||
if (key.includes("doc")) return "Mock document content";
|
||||
|
|
@ -477,7 +477,7 @@ describe("ProjectContextCache", () => {
|
|||
} as unknown as ProjectConfig;
|
||||
|
||||
// Mock cache non-existence for this isolated project
|
||||
mockApp.vault.adapter.exists.mockImplementation((path) => {
|
||||
mockApp.vault.adapter.exists.mockImplementation((path: string) => {
|
||||
if (path.includes("isolated-test-project-id")) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ describe("FileTreeTools", () => {
|
|||
|
||||
it("should exclude files based on patterns", async () => {
|
||||
// Mock shouldIndexFile to exclude all files in projects folder
|
||||
(searchUtils.shouldIndexFile as jest.Mock).mockImplementation((file) => {
|
||||
(searchUtils.shouldIndexFile as jest.Mock).mockImplementation((file: { path: string }) => {
|
||||
return !file.path.includes("projects");
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue