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
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> {
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 { NoteTools } from './note-tools';
import { VaultTools } from './vault-tools';
import { createNoteTools } from './note-tools-factory';
import { createVaultTools } from './vault-tools-factory';
import { NotificationManager } from '../ui/notifications';
@@ -11,7 +12,7 @@ export class ToolRegistry {
private notificationManager: NotificationManager | null = null;
constructor(app: App) {
this.noteTools = new NoteTools(app);
this.noteTools = createNoteTools(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

@@ -1,7 +1,7 @@
import { App, TFile } from 'obsidian';
import {
CallToolResult,
ParsedNote,
import {
CallToolResult,
ParsedNote,
ExcalidrawMetadata,
UpdateFrontmatterResult,
UpdateSectionsResult,
@@ -16,9 +16,14 @@ import { ErrorMessages } from '../utils/error-messages';
import { FrontmatterUtils } from '../utils/frontmatter-utils';
import { WaypointUtils } from '../utils/waypoint-utils';
import { VersionUtils } from '../utils/version-utils';
import { IVaultAdapter, IFileManagerAdapter } from '../adapters/interfaces';
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(
path: string,
@@ -67,7 +72,7 @@ export class NoteTools {
}
try {
const content = await this.app.vault.read(file);
const content = await this.vault.read(file);
// If no special options, return simple content
if (!parseFrontmatter) {
@@ -145,7 +150,7 @@ export class NoteTools {
// Delete existing file before creating
const existingFile = PathUtils.resolveFile(this.app, normalizedPath);
if (existingFile) {
await this.app.vault.delete(existingFile);
await this.vault.delete(existingFile);
}
} else if (onConflict === 'rename') {
// Generate a unique name
@@ -198,7 +203,7 @@ export class NoteTools {
// Proceed with file creation
try {
const file = await this.app.vault.create(finalPath, content);
const file = await this.vault.create(finalPath, content);
const result: CreateNoteResult = {
success: true,
@@ -252,7 +257,7 @@ export class NoteTools {
// Create the current folder if it doesn't exist
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 {
// 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);
if (waypointCheck.affected) {
@@ -313,7 +318,7 @@ export class NoteTools {
};
}
await this.app.vault.modify(file, content);
await this.vault.modify(file, content);
return {
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)
// 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
const renamedFile = PathUtils.resolveFile(this.app, normalizedNewPath);
@@ -524,11 +529,11 @@ export class NoteTools {
// Perform actual deletion
if (soft) {
// 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}`;
} else {
// Permanent deletion
await this.app.vault.delete(file);
await this.vault.delete(file);
}
const result: DeleteNoteResult = {
@@ -595,7 +600,7 @@ export class NoteTools {
}
try {
const content = await this.app.vault.read(file);
const content = await this.vault.read(file);
// Parse Excalidraw metadata (gracefully handles malformed files)
const metadata = FrontmatterUtils.parseExcalidrawMetadata(content);
@@ -725,7 +730,7 @@ export class NoteTools {
}
// Read current content
const content = await this.app.vault.read(file);
const content = await this.vault.read(file);
const extracted = FrontmatterUtils.extractFrontmatter(content);
// Get current frontmatter or create new
@@ -767,7 +772,7 @@ export class NoteTools {
}
// Write back
await this.app.vault.modify(file, newContent);
await this.vault.modify(file, newContent);
// Generate response with version info
const result: UpdateFrontmatterResult = {
@@ -851,7 +856,7 @@ export class NoteTools {
}
// Read current content
const content = await this.app.vault.read(file);
const content = await this.vault.read(file);
const lines = content.split('\n');
// 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');
// Write back
await this.app.vault.modify(file, newContent);
await this.vault.modify(file, newContent);
// Generate response with version info
const result: UpdateSectionsResult = {