Add dry-run mode, target folder filtering, and tag normalization
- Add `dryRun` option to preview proposed tag/link changes without applying - Add `targetFolder` config to restrict auto-tagging/linking to specific paths - Add `normalizeTags` with vocabulary building to canonicalize generated tags against existing vault tags - Update settings UI with new toggles and text inputs for both auto-tag and auto-link sections
This commit is contained in:
+174
-6
@@ -9,6 +9,9 @@ export interface AutoOrganizeConfig {
|
||||
minNoteLength: number;
|
||||
maxNoteLength: number;
|
||||
tagPromptTemplate: string;
|
||||
dryRun: boolean;
|
||||
targetFolder: string;
|
||||
normalizeTags: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_AUTO_ORGANIZE_CONFIG: AutoOrganizeConfig = {
|
||||
@@ -18,8 +21,96 @@ export const DEFAULT_AUTO_ORGANIZE_CONFIG: AutoOrganizeConfig = {
|
||||
maxNoteLength: 8000,
|
||||
tagPromptTemplate:
|
||||
'Given the following note, suggest {{maxTags}} relevant, concise tags that describe its content.\n\nReturn ONLY a comma-separated list of tags (no quotes, no numbering, no explanations).\n\nTitle: {{title}}\n\nContent:\n{{content}}',
|
||||
dryRun: false,
|
||||
targetFolder: '',
|
||||
normalizeTags: true,
|
||||
};
|
||||
|
||||
export interface ProposedTagChange {
|
||||
file: TFile;
|
||||
proposedTags: string[];
|
||||
currentTags?: string[];
|
||||
}
|
||||
|
||||
export interface ProposedLinkChange {
|
||||
file: TFile;
|
||||
relatedNotes: { path: string; title: string; score: number }[];
|
||||
}
|
||||
|
||||
export interface DryRunResult {
|
||||
tagChanges: ProposedTagChange[];
|
||||
linkChanges: ProposedLinkChange[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a tag string to lowercase, hyphenated, trimmed form.
|
||||
*/
|
||||
export function normalizeTag(raw: string): string {
|
||||
return raw
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a vocabulary map from existing vault tags.
|
||||
* Maps normalized tag -> preferred canonical form (first seen).
|
||||
*/
|
||||
export function buildTagVocabulary(vault: Vault, app: App): Map<string, string> {
|
||||
const vocab = new Map<string, string>();
|
||||
const files = vault.getMarkdownFiles();
|
||||
for (const file of files) {
|
||||
try {
|
||||
const cache = app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cache?.frontmatter;
|
||||
const rawTags: unknown = frontmatter?.tags;
|
||||
const tagList: string[] = [];
|
||||
if (Array.isArray(rawTags)) {
|
||||
tagList.push(...rawTags.map(String));
|
||||
} else if (typeof rawTags === 'string') {
|
||||
tagList.push(
|
||||
...rawTags
|
||||
.split(/[,\n]+/)
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0)
|
||||
);
|
||||
}
|
||||
for (const tag of tagList) {
|
||||
const norm = normalizeTag(tag);
|
||||
if (norm.length > 0 && !vocab.has(norm)) {
|
||||
vocab.set(norm, tag);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
return vocab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a list of tags against a vocabulary.
|
||||
*/
|
||||
export function normalizeTagsAgainstVocabulary(
|
||||
tags: string[],
|
||||
vocab: Map<string, string>
|
||||
): string[] {
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const tag of tags) {
|
||||
const norm = normalizeTag(tag);
|
||||
if (seen.has(norm)) continue;
|
||||
seen.add(norm);
|
||||
// Use canonical form if in vocabulary, otherwise use normalized form
|
||||
const canonical = vocab.get(norm);
|
||||
result.push(canonical ?? norm);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically tags untagged notes using the AI model.
|
||||
*/
|
||||
@@ -46,6 +137,18 @@ export class AutoTagger {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is inside the target folder.
|
||||
*/
|
||||
private isInTargetFolder(file: TFile): boolean {
|
||||
if (!this.config.targetFolder || this.config.targetFolder.trim().length === 0) {
|
||||
return true;
|
||||
}
|
||||
const target = this.config.targetFolder.replace(/\/$/, '').trim();
|
||||
const fileFolder = file.path.split('/').slice(0, -1).join('/');
|
||||
return fileFolder === target || fileFolder.startsWith(`${target}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a note has meaningful tags using metadataCache.
|
||||
*/
|
||||
@@ -70,13 +173,14 @@ export class AutoTagger {
|
||||
|
||||
/**
|
||||
* Find all markdown files that lack a `tags` frontmatter field.
|
||||
* Respects targetFolder config.
|
||||
*/
|
||||
getUntaggedNotes(): TFile[] {
|
||||
const files = this.vault.getMarkdownFiles();
|
||||
const untagged: TFile[] = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (!this.hasTags(file)) {
|
||||
if (!this.hasTags(file) && this.isInTargetFolder(file)) {
|
||||
untagged.push(file);
|
||||
}
|
||||
} catch {
|
||||
@@ -107,7 +211,13 @@ export class AutoTagger {
|
||||
|
||||
const response = await this.ollamaClient.chat([{ role: 'user', content: prompt }]);
|
||||
|
||||
const tags = this.parseTagResponse(response.content);
|
||||
let tags = this.parseTagResponse(response.content);
|
||||
|
||||
if (this.config.normalizeTags) {
|
||||
const vocab = buildTagVocabulary(this.vault, this.app);
|
||||
tags = normalizeTagsAgainstVocabulary(tags, vocab);
|
||||
}
|
||||
|
||||
Logger.info(`Generated tags for ${file.path}: ${tags.join(', ')}`, 'auto-tagger');
|
||||
return tags;
|
||||
} catch (error) {
|
||||
@@ -167,8 +277,9 @@ export class AutoTagger {
|
||||
|
||||
/**
|
||||
* Run auto-tagging on all untagged notes.
|
||||
* If dryRun is enabled, returns proposed changes without applying.
|
||||
*/
|
||||
async run(): Promise<{ tagged: number; skipped: number }> {
|
||||
async run(): Promise<{ tagged: number; skipped: number; dryRun?: ProposedTagChange[] }> {
|
||||
if (!this.config.enabled) {
|
||||
new Notice('Auto-tagging is disabled in settings.');
|
||||
return { tagged: 0, skipped: 0 };
|
||||
@@ -180,6 +291,23 @@ export class AutoTagger {
|
||||
return { tagged: 0, skipped: 0 };
|
||||
}
|
||||
|
||||
if (this.config.dryRun) {
|
||||
new Notice(`Dry-run: evaluating ${untagged.length} notes...`);
|
||||
const proposals: ProposedTagChange[] = [];
|
||||
let skipped = 0;
|
||||
for (const file of untagged) {
|
||||
const tags = await this.generateTags(file);
|
||||
if (tags.length > 0) {
|
||||
proposals.push({ file, proposedTags: tags });
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
}
|
||||
new Notice(`Dry-run complete: ${proposals.length} proposed tag changes, ${skipped} skipped.`);
|
||||
return { tagged: proposals.length, skipped, dryRun: proposals };
|
||||
}
|
||||
|
||||
new Notice(`Auto-tagging ${untagged.length} notes...`);
|
||||
let tagged = 0;
|
||||
let skipped = 0;
|
||||
@@ -216,15 +344,18 @@ export class AutoLinker {
|
||||
private vault: Vault;
|
||||
private vaultIndexer: VaultIndexer;
|
||||
private config: { enabled: boolean; maxLinksPerNote: number; similarityThreshold: number };
|
||||
private targetFolder: string;
|
||||
|
||||
constructor(
|
||||
vault: Vault,
|
||||
vaultIndexer: VaultIndexer,
|
||||
config: { enabled: boolean; maxLinksPerNote: number; similarityThreshold: number }
|
||||
config: { enabled: boolean; maxLinksPerNote: number; similarityThreshold: number },
|
||||
targetFolder: string = ''
|
||||
) {
|
||||
this.vault = vault;
|
||||
this.vaultIndexer = vaultIndexer;
|
||||
this.config = config;
|
||||
this.targetFolder = targetFolder;
|
||||
}
|
||||
|
||||
updateConfig(config: {
|
||||
@@ -235,6 +366,22 @@ export class AutoLinker {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
setTargetFolder(folder: string): void {
|
||||
this.targetFolder = folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is inside the target folder.
|
||||
*/
|
||||
private isInTargetFolder(file: TFile): boolean {
|
||||
if (!this.targetFolder || this.targetFolder.trim().length === 0) {
|
||||
return true;
|
||||
}
|
||||
const target = this.targetFolder.replace(/\/$/, '').trim();
|
||||
const fileFolder = file.path.split('/').slice(0, -1).join('/');
|
||||
return fileFolder === target || fileFolder.startsWith(`${target}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find related notes for a given file using semantic search.
|
||||
*/
|
||||
@@ -290,14 +437,35 @@ export class AutoLinker {
|
||||
|
||||
/**
|
||||
* Run auto-linking on all notes.
|
||||
* If dryRun is enabled, returns proposed changes without applying.
|
||||
*/
|
||||
async run(): Promise<{ linked: number; skipped: number }> {
|
||||
async run(
|
||||
dryRun = false
|
||||
): Promise<{ linked: number; skipped: number; dryRun?: ProposedLinkChange[] }> {
|
||||
if (!this.config.enabled) {
|
||||
new Notice('Auto-linking is disabled in settings.');
|
||||
return { linked: 0, skipped: 0 };
|
||||
}
|
||||
|
||||
const files = this.vault.getMarkdownFiles();
|
||||
const files = this.vault.getMarkdownFiles().filter((f) => this.isInTargetFolder(f));
|
||||
|
||||
if (dryRun) {
|
||||
new Notice(`Dry-run: evaluating ${files.length} notes for links...`);
|
||||
const proposals: ProposedLinkChange[] = [];
|
||||
let skipped = 0;
|
||||
for (const file of files) {
|
||||
const related = await this.findRelatedNotes(file);
|
||||
if (related.length > 0) {
|
||||
proposals.push({ file, relatedNotes: related });
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
new Notice(`Dry-run complete: ${proposals.length} proposed link changes.`);
|
||||
return { linked: proposals.length, skipped, dryRun: proposals };
|
||||
}
|
||||
|
||||
new Notice(`Auto-linking ${files.length} notes...`);
|
||||
|
||||
let linked = 0;
|
||||
|
||||
@@ -27,10 +27,15 @@ export const DEFAULT_SETTINGS = {
|
||||
maxNoteLength: 8000,
|
||||
tagPromptTemplate:
|
||||
'Given the following note, suggest {{maxTags}} relevant, concise tags that describe its content.\n\nReturn ONLY a comma-separated list of tags (no quotes, no numbering, no explanations).\n\nTitle: {{title}}\n\nContent:\n{{content}}',
|
||||
dryRun: false,
|
||||
targetFolder: '',
|
||||
normalizeTags: true,
|
||||
},
|
||||
autoLinkConfig: {
|
||||
enabled: false,
|
||||
maxLinksPerNote: 3,
|
||||
similarityThreshold: 0.6,
|
||||
targetFolder: '',
|
||||
dryRun: false,
|
||||
},
|
||||
};
|
||||
|
||||
+45
-1
@@ -159,7 +159,7 @@ export default class OllamaPlugin extends Plugin {
|
||||
|
||||
if (!this.autoLinker) {
|
||||
const vaultIndexer = new VaultIndexer(this.app.vault, undefined, this.vaultVectorStore);
|
||||
this.autoLinker = new AutoLinker(this.app.vault, vaultIndexer, this.settings.autoLinkConfig);
|
||||
this.autoLinker = new AutoLinker(this.app.vault, vaultIndexer, this.settings.autoLinkConfig, this.settings.autoLinkConfig.targetFolder);
|
||||
} else {
|
||||
this.autoLinker.updateConfig(this.settings.autoLinkConfig);
|
||||
}
|
||||
@@ -709,6 +709,28 @@ class OllamaSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Normalize Tags')
|
||||
.setDesc('Normalize generated tags against existing vault tag vocabulary (e.g., prefer "machine-learning" over "machine learning")')
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.autoTagConfig.normalizeTags).onChange(async (value) => {
|
||||
this.plugin.settings.autoTagConfig.normalizeTags = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Target Folder (Auto-Tag)')
|
||||
.setDesc('Only auto-tag notes inside this folder path. Leave empty for all notes.')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(this.plugin.settings.autoTagConfig.targetFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoTagConfig.targetFolder = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Tag Prompt Template')
|
||||
.setDesc(
|
||||
@@ -769,6 +791,28 @@ class OllamaSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Target Folder (Auto-Link)')
|
||||
.setDesc('Only add related links to notes inside this folder path. Leave empty for all notes.')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(this.plugin.settings.autoLinkConfig.targetFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoLinkConfig.targetFolder = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Dry Run Mode')
|
||||
.setDesc('Preview proposed link changes without applying them')
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.autoLinkConfig.dryRun).onChange(async (value) => {
|
||||
this.plugin.settings.autoLinkConfig.dryRun = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-Link Similarity Threshold')
|
||||
.setDesc('Minimum similarity score for notes to be considered related (default: 0.6)')
|
||||
|
||||
@@ -239,11 +239,16 @@ export interface PluginSettings {
|
||||
minNoteLength: number;
|
||||
maxNoteLength: number;
|
||||
tagPromptTemplate: string;
|
||||
dryRun: boolean;
|
||||
targetFolder: string;
|
||||
normalizeTags: boolean;
|
||||
};
|
||||
autoLinkConfig: {
|
||||
enabled: boolean;
|
||||
maxLinksPerNote: number;
|
||||
similarityThreshold: number;
|
||||
targetFolder: string;
|
||||
dryRun: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { AutoTagger, AutoLinker } from '../src/auto-organizer';
|
||||
import {
|
||||
AutoTagger,
|
||||
AutoLinker,
|
||||
normalizeTag,
|
||||
buildTagVocabulary,
|
||||
normalizeTagsAgainstVocabulary,
|
||||
} from '../src/auto-organizer';
|
||||
import { OllamaClient } from '../src/ollama-client';
|
||||
|
||||
// Mock dependencies
|
||||
@@ -45,6 +51,9 @@ describe('AutoTagger', () => {
|
||||
minNoteLength: 50,
|
||||
maxNoteLength: 8000,
|
||||
tagPromptTemplate: 'Tags for {{title}}: {{content}}',
|
||||
dryRun: false,
|
||||
targetFolder: '',
|
||||
normalizeTags: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +96,24 @@ describe('AutoTagger', () => {
|
||||
const result = await tagger.getUntaggedNotes();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should respect targetFolder', async () => {
|
||||
tagger.updateConfig({ ...(tagger as any).config, targetFolder: 'Projects' });
|
||||
const files = [
|
||||
{ path: 'Projects/note1.md' },
|
||||
{ path: 'Archive/note2.md' },
|
||||
{ path: 'Projects/Sub/note3.md' },
|
||||
] as any[];
|
||||
mockGetMarkdownFiles.mockReturnValue(files);
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(null);
|
||||
|
||||
const result = await tagger.getUntaggedNotes();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((f: any) => f.path)).toEqual([
|
||||
'Projects/note1.md',
|
||||
'Projects/Sub/note3.md',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTagResponse', () => {
|
||||
@@ -160,6 +187,90 @@ describe('AutoTagger', () => {
|
||||
const result = await tagger.run();
|
||||
expect(result.skipped).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should return dry-run proposals when dryRun is enabled', async () => {
|
||||
tagger.updateConfig({ ...tagger['config'], dryRun: true });
|
||||
const files = [{ path: 'note.md', basename: 'Note' }] as any[];
|
||||
mockGetMarkdownFiles.mockReturnValue(files);
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(null);
|
||||
mockRead.mockResolvedValue('A longer note about AI and machine learning with lots of interesting content that exceeds the minimum length requirement for tagging.');
|
||||
|
||||
// Mock the ollamaClient on the tagger instance
|
||||
(tagger as any).ollamaClient = {
|
||||
chat: jest.fn().mockResolvedValue({
|
||||
content: 'ai, machine-learning',
|
||||
role: 'assistant',
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await tagger.run();
|
||||
expect(result.dryRun).toBeDefined();
|
||||
expect(result.dryRun!.length).toBe(1);
|
||||
expect(result.dryRun![0].proposedTags).toContain('ai');
|
||||
expect(result.dryRun![0].proposedTags).toContain('machine-learning');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeTag', () => {
|
||||
it('should lowercase and hyphenate tags', () => {
|
||||
expect(normalizeTag('Machine Learning')).toBe('machine-learning');
|
||||
expect(normalizeTag('AI')).toBe('ai');
|
||||
expect(normalizeTag('obsidian-plugin')).toBe('obsidian-plugin');
|
||||
});
|
||||
|
||||
it('should strip special characters', () => {
|
||||
expect(normalizeTag('C++')).toBe('c');
|
||||
expect(normalizeTag('Node.js')).toBe('nodejs');
|
||||
});
|
||||
|
||||
it('should trim dashes', () => {
|
||||
expect(normalizeTag('-leading')).toBe('leading');
|
||||
expect(normalizeTag('trailing-')).toBe('trailing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTagVocabulary', () => {
|
||||
it('should collect existing tags from vault frontmatter', () => {
|
||||
const mockFiles = [{ path: 'a.md' }, { path: 'b.md' }] as any[];
|
||||
const vault = createMockVault();
|
||||
vault.getMarkdownFiles.mockReturnValue(mockFiles);
|
||||
;(vault as any).app = {
|
||||
metadataCache: {
|
||||
getFileCache: jest.fn().mockImplementation((f: any) => {
|
||||
if (f.path === 'a.md') return { frontmatter: { tags: ['machine-learning', 'ai'] } };
|
||||
if (f.path === 'b.md') return { frontmatter: { tags: 'obsidian-plugin' } };
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const vocab = buildTagVocabulary(vault as any, (vault as any).app);
|
||||
expect(vocab.get('machine-learning')).toBe('machine-learning');
|
||||
expect(vocab.get('ai')).toBe('ai');
|
||||
expect(vocab.get('obsidian-plugin')).toBe('obsidian-plugin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeTagsAgainstVocabulary', () => {
|
||||
it('should prefer canonical forms from vocabulary', () => {
|
||||
const vocab = new Map([
|
||||
['machine-learning', 'machine-learning'],
|
||||
['obsidian', 'Obsidian'],
|
||||
]);
|
||||
const result = normalizeTagsAgainstVocabulary(
|
||||
['Machine Learning', 'obsidian', 'new-tag'],
|
||||
vocab
|
||||
);
|
||||
expect(result).toContain('machine-learning');
|
||||
expect(result).toContain('Obsidian');
|
||||
expect(result).toContain('new-tag');
|
||||
});
|
||||
|
||||
it('should deduplicate normalized tags', () => {
|
||||
const vocab = new Map();
|
||||
const result = normalizeTagsAgainstVocabulary(['ai', 'AI', 'Ai'], vocab);
|
||||
expect(result).toEqual(['ai']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -242,5 +353,32 @@ describe('AutoLinker', () => {
|
||||
const result = await linker.run();
|
||||
expect(result.linked).toBe(0);
|
||||
});
|
||||
|
||||
it('should respect targetFolder', async () => {
|
||||
linker.setTargetFolder('Projects');
|
||||
const files = [{ path: 'Projects/note1.md' }, { path: 'Archive/note2.md' }] as any[];
|
||||
mockVault.getMarkdownFiles.mockReturnValue(files);
|
||||
mockVault.read.mockResolvedValue('Content');
|
||||
mockIndexer.searchVault!.mockResolvedValue([]);
|
||||
|
||||
const result = await linker.run();
|
||||
expect(mockVault.read).toHaveBeenCalledTimes(1);
|
||||
expect(mockVault.read).toHaveBeenCalledWith(files[0]);
|
||||
});
|
||||
|
||||
it('should return dry-run proposals when dryRun is true', async () => {
|
||||
const files = [{ path: 'note.md' }] as any[];
|
||||
mockVault.getMarkdownFiles.mockReturnValue(files);
|
||||
mockVault.read.mockResolvedValue('Content');
|
||||
mockIndexer.searchVault!.mockResolvedValue([
|
||||
{ path: 'other.md', title: 'Other', score: 0.9, content: '' },
|
||||
]);
|
||||
|
||||
const result = await linker.run(true);
|
||||
expect(result.dryRun).toBeDefined();
|
||||
expect(result.dryRun!.length).toBe(1);
|
||||
expect(result.dryRun![0].relatedNotes[0].path).toBe('other.md');
|
||||
expect(mockVault.modify).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,11 +59,16 @@ const mockSettings: PluginSettings = {
|
||||
minNoteLength: 50,
|
||||
maxNoteLength: 8000,
|
||||
tagPromptTemplate: 'Tags: {{content}}',
|
||||
dryRun: false,
|
||||
targetFolder: '',
|
||||
normalizeTags: true,
|
||||
},
|
||||
autoLinkConfig: {
|
||||
enabled: false,
|
||||
maxLinksPerNote: 3,
|
||||
similarityThreshold: 0.6,
|
||||
targetFolder: '',
|
||||
dryRun: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user