refactor: migrate NoteTools to use adapter pattern

Implements Task 10 from the implementation plan:

1. Updated NoteTools constructor to accept IVaultAdapter and IFileManagerAdapter
2. Created note-tools-factory.ts with factory function
3. Migrated all CRUD methods to use adapters:
   - readNote: uses vault.read()
   - createNote: uses vault.create(), vault.delete()
   - createParentFolders: uses vault.createFolder()
   - updateNote: uses vault.read(), vault.modify()
   - deleteNote: uses vault.trash(), vault.delete()
   - renameFile: uses fileManager.renameFile()
   - readExcalidraw: uses vault.read()
   - updateFrontmatter: uses vault.read(), vault.modify()
   - updateSections: uses vault.read(), vault.modify()
4. Extended IVaultAdapter interface with modify(), delete(), and trash() methods
5. Implemented new methods in VaultAdapter
6. Updated ToolRegistry to use factory function
7. Kept app parameter temporarily for PathUtils dependency
8. All methods now use adapters instead of direct Obsidian API calls
9. Code compiles successfully

This change enables 100% test coverage by allowing full mocking of vault operations.
This commit is contained in:
2025-10-19 23:53:48 -04:00
parent aca4d35944
commit 0185ca7d00
5 changed files with 59 additions and 19 deletions

View File

@@ -25,6 +25,13 @@ export interface IVaultAdapter {
// File creation // File creation
create(path: string, data: string): Promise<TFile>; create(path: string, data: string): Promise<TFile>;
// File modification
modify(file: TFile, data: string): Promise<void>;
// File deletion
delete(file: TAbstractFile): Promise<void>;
trash(file: TAbstractFile, system: boolean): Promise<void>;
} }
/** /**

View File

@@ -38,4 +38,16 @@ export class VaultAdapter implements IVaultAdapter {
async create(path: string, data: string): Promise<TFile> { async create(path: string, data: string): Promise<TFile> {
return this.vault.create(path, data); return this.vault.create(path, data);
} }
async modify(file: TFile, data: string): Promise<void> {
await this.vault.modify(file, data);
}
async delete(file: TAbstractFile): Promise<void> {
await this.vault.delete(file);
}
async trash(file: TAbstractFile, system: boolean): Promise<void> {
await this.vault.trash(file, system);
}
} }

View File

@@ -2,6 +2,7 @@ import { App } from 'obsidian';
import { Tool, CallToolResult } from '../types/mcp-types'; import { Tool, CallToolResult } from '../types/mcp-types';
import { NoteTools } from './note-tools'; import { NoteTools } from './note-tools';
import { VaultTools } from './vault-tools'; import { VaultTools } from './vault-tools';
import { createNoteTools } from './note-tools-factory';
import { createVaultTools } from './vault-tools-factory'; import { createVaultTools } from './vault-tools-factory';
import { NotificationManager } from '../ui/notifications'; import { NotificationManager } from '../ui/notifications';
@@ -11,7 +12,7 @@ export class ToolRegistry {
private notificationManager: NotificationManager | null = null; private notificationManager: NotificationManager | null = null;
constructor(app: App) { constructor(app: App) {
this.noteTools = new NoteTools(app); this.noteTools = createNoteTools(app);
this.vaultTools = createVaultTools(app); this.vaultTools = createVaultTools(app);
} }

View File

@@ -0,0 +1,15 @@
import { App } from 'obsidian';
import { NoteTools } from './note-tools';
import { VaultAdapter } from '../adapters/vault-adapter';
import { FileManagerAdapter } from '../adapters/file-manager-adapter';
/**
* Factory function to create NoteTools with concrete adapters
*/
export function createNoteTools(app: App): NoteTools {
return new NoteTools(
new VaultAdapter(app.vault),
new FileManagerAdapter(app.fileManager),
app
);
}

View File

@@ -16,9 +16,14 @@ import { ErrorMessages } from '../utils/error-messages';
import { FrontmatterUtils } from '../utils/frontmatter-utils'; import { FrontmatterUtils } from '../utils/frontmatter-utils';
import { WaypointUtils } from '../utils/waypoint-utils'; import { WaypointUtils } from '../utils/waypoint-utils';
import { VersionUtils } from '../utils/version-utils'; import { VersionUtils } from '../utils/version-utils';
import { IVaultAdapter, IFileManagerAdapter } from '../adapters/interfaces';
export class NoteTools { export class NoteTools {
constructor(private app: App) {} constructor(
private vault: IVaultAdapter,
private fileManager: IFileManagerAdapter,
private app: App // Keep temporarily for methods not yet migrated
) {}
async readNote( async readNote(
path: string, path: string,
@@ -67,7 +72,7 @@ export class NoteTools {
} }
try { try {
const content = await this.app.vault.read(file); const content = await this.vault.read(file);
// If no special options, return simple content // If no special options, return simple content
if (!parseFrontmatter) { if (!parseFrontmatter) {
@@ -145,7 +150,7 @@ export class NoteTools {
// Delete existing file before creating // Delete existing file before creating
const existingFile = PathUtils.resolveFile(this.app, normalizedPath); const existingFile = PathUtils.resolveFile(this.app, normalizedPath);
if (existingFile) { if (existingFile) {
await this.app.vault.delete(existingFile); await this.vault.delete(existingFile);
} }
} else if (onConflict === 'rename') { } else if (onConflict === 'rename') {
// Generate a unique name // Generate a unique name
@@ -198,7 +203,7 @@ export class NoteTools {
// Proceed with file creation // Proceed with file creation
try { try {
const file = await this.app.vault.create(finalPath, content); const file = await this.vault.create(finalPath, content);
const result: CreateNoteResult = { const result: CreateNoteResult = {
success: true, success: true,
@@ -252,7 +257,7 @@ export class NoteTools {
// Create the current folder if it doesn't exist // Create the current folder if it doesn't exist
if (!PathUtils.pathExists(this.app, path)) { if (!PathUtils.pathExists(this.app, path)) {
await this.app.vault.createFolder(path); await this.vault.createFolder(path);
} }
} }
@@ -292,7 +297,7 @@ export class NoteTools {
try { try {
// Check for waypoint edit protection // Check for waypoint edit protection
const currentContent = await this.app.vault.read(file); const currentContent = await this.vault.read(file);
const waypointCheck = WaypointUtils.wouldAffectWaypoint(currentContent, content); const waypointCheck = WaypointUtils.wouldAffectWaypoint(currentContent, content);
if (waypointCheck.affected) { if (waypointCheck.affected) {
@@ -313,7 +318,7 @@ export class NoteTools {
}; };
} }
await this.app.vault.modify(file, content); await this.vault.modify(file, content);
return { return {
content: [{ type: "text", text: `Note updated successfully: ${file.path}` }] content: [{ type: "text", text: `Note updated successfully: ${file.path}` }]
}; };
@@ -424,7 +429,7 @@ export class NoteTools {
// Use Obsidian's FileManager to rename (automatically updates links) // Use Obsidian's FileManager to rename (automatically updates links)
// Note: Obsidian's renameFile automatically updates all wikilinks // Note: Obsidian's renameFile automatically updates all wikilinks
await this.app.fileManager.renameFile(file, normalizedNewPath); await this.fileManager.renameFile(file, normalizedNewPath);
// Get the renamed file to get version info // Get the renamed file to get version info
const renamedFile = PathUtils.resolveFile(this.app, normalizedNewPath); const renamedFile = PathUtils.resolveFile(this.app, normalizedNewPath);
@@ -524,11 +529,11 @@ export class NoteTools {
// Perform actual deletion // Perform actual deletion
if (soft) { if (soft) {
// Move to trash using Obsidian's trash method // Move to trash using Obsidian's trash method
await this.app.vault.trash(file, true); await this.vault.trash(file, true);
destination = `.trash/${file.name}`; destination = `.trash/${file.name}`;
} else { } else {
// Permanent deletion // Permanent deletion
await this.app.vault.delete(file); await this.vault.delete(file);
} }
const result: DeleteNoteResult = { const result: DeleteNoteResult = {
@@ -595,7 +600,7 @@ export class NoteTools {
} }
try { try {
const content = await this.app.vault.read(file); const content = await this.vault.read(file);
// Parse Excalidraw metadata (gracefully handles malformed files) // Parse Excalidraw metadata (gracefully handles malformed files)
const metadata = FrontmatterUtils.parseExcalidrawMetadata(content); const metadata = FrontmatterUtils.parseExcalidrawMetadata(content);
@@ -725,7 +730,7 @@ export class NoteTools {
} }
// Read current content // Read current content
const content = await this.app.vault.read(file); const content = await this.vault.read(file);
const extracted = FrontmatterUtils.extractFrontmatter(content); const extracted = FrontmatterUtils.extractFrontmatter(content);
// Get current frontmatter or create new // Get current frontmatter or create new
@@ -767,7 +772,7 @@ export class NoteTools {
} }
// Write back // Write back
await this.app.vault.modify(file, newContent); await this.vault.modify(file, newContent);
// Generate response with version info // Generate response with version info
const result: UpdateFrontmatterResult = { const result: UpdateFrontmatterResult = {
@@ -851,7 +856,7 @@ export class NoteTools {
} }
// Read current content // Read current content
const content = await this.app.vault.read(file); const content = await this.vault.read(file);
const lines = content.split('\n'); const lines = content.split('\n');
// Sort edits by startLine in descending order to apply from bottom to top // Sort edits by startLine in descending order to apply from bottom to top
@@ -891,7 +896,7 @@ export class NoteTools {
const newContent = lines.join('\n'); const newContent = lines.join('\n');
// Write back // Write back
await this.app.vault.modify(file, newContent); await this.vault.modify(file, newContent);
// Generate response with version info // Generate response with version info
const result: UpdateSectionsResult = { const result: UpdateSectionsResult = {