/**
* Tests for cleanup suggestions service
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import {
generateCleanupSuggestions,
generateReplaceCleanupSuggestions,
generateDeletionCleanupSuggestions,
} from '../../../src/services/operations/cleanup.js';
describe('cleanup suggestions', () => {
let db: Database.Database;
beforeEach(() => {
db = new Database(':memory:');
db.exec(`
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE,
title TEXT,
type TEXT,
related TEXT DEFAULT '[]'
)
`);
db.exec(`
CREATE TABLE links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id INTEGER,
target_path TEXT
)
`);
});
afterEach(() => {
db.close();
});
function insertNote(path: string, title: string, type?: string) {
db.prepare('INSERT INTO notes (path, title, type) VALUES (?, ?, ?)').run(path, title, type ?? 'research');
}
function insertLink(sourceId: number, targetPath: string) {
db.prepare('INSERT INTO links (source_id, target_path) VALUES (?, ?)').run(sourceId, targetPath);
}
describe('generateCleanupSuggestions', () => {
it('returns undefined when no targetDir', () => {
const result = generateCleanupSuggestions(db);
expect(result).toBeUndefined();
});
it('returns undefined when no orphans found', () => {
insertNote('research/note.md', 'Note');
// Give it outgoing links so it's not an orphan
insertLink(1, 'Other Note');
const result = generateCleanupSuggestions(db, { targetDir: 'research' });
expect(result).toBeUndefined();
});
it('detects orphaned files in directory', () => {
// An isolated note (no links in or out)
insertNote('research/orphan.md', 'Orphan');
const result = generateCleanupSuggestions(db, { targetDir: 'research' });
expect(result).toBeDefined();
expect(result!.orphaned_files).toContain('research/orphan.md');
expect(result!.message).toContain('orphaned');
});
it('excludes specified paths from orphan detection', () => {
insertNote('research/new-note.md', 'New Note');
const result = generateCleanupSuggestions(db, {
targetDir: 'research',
excludePaths: ['research/new-note.md'],
});
expect(result).toBeUndefined();
});
it('checks stale children when enabled', () => {
// A note with related links but no matching hub
insertNote('research/child.md', 'Child', 'research');
db.prepare('UPDATE notes SET related = ? WHERE path = ?')
.run('["[[Research Hub]]"]', 'research/child.md');
const result = generateCleanupSuggestions(db, {
targetDir: 'research',
checkStaleChildren: true,
});
// May or may not detect stale children depending on query match
// The important thing is no crash and the option is passed through
expect(result === undefined || result.message !== undefined).toBe(true);
});
it('does not check stale children by default', () => {
insertNote('research/child.md', 'Child', 'research');
const result = generateCleanupSuggestions(db, { targetDir: 'research' });
// Should only report orphans, not stale children
if (result) {
expect(result.stale_children).toBeUndefined();
}
});
});
describe('generateReplaceCleanupSuggestions', () => {
it('returns undefined when replaced file was not a hub', () => {
const result = generateReplaceCleanupSuggestions(db, 'research/note.md', false);
expect(result).toBeUndefined();
});
it('returns undefined when hub has no stale children', () => {
insertNote('research/Hub.md', 'Hub', 'research_hub');
const result = generateReplaceCleanupSuggestions(db, 'research/Hub.md', true);
expect(result).toBeUndefined();
});
it('detects stale children when hub was replaced', () => {
// Create a note that looks like a child with related links
insertNote('research/child.md', 'Child', 'research');
db.prepare('UPDATE notes SET related = ? WHERE path = ?')
.run('["[[Hub]]"]', 'research/child.md');
const result = generateReplaceCleanupSuggestions(db, 'research/Hub.md', true);
// The query behavior depends on whether the child pattern matches
// This tests the code path without crashing
expect(result === undefined || result.stale_children !== undefined).toBe(true);
});
});
describe('generateDeletionCleanupSuggestions', () => {
it('returns undefined for empty deleted paths', () => {
const result = generateDeletionCleanupSuggestions(db, []);
expect(result).toBeUndefined();
});
it('returns undefined when no broken links exist', () => {
insertNote('remaining.md', 'Remaining');
const result = generateDeletionCleanupSuggestions(db, ['deleted.md']);
expect(result).toBeUndefined();
});
it('detects files with broken links after deletion', () => {
insertNote('source.md', 'Source');
// source.md links to "Deleted Note"
insertLink(1, 'Deleted Note');
const result = generateDeletionCleanupSuggestions(db, ['path/Deleted Note.md']);
expect(result).toBeDefined();
expect(result!.broken_links).toContain('source.md');
expect(result!.message).toContain('broken links');
});
it('handles multiple deleted paths', () => {
insertNote('source1.md', 'Source 1');
insertNote('source2.md', 'Source 2');
insertLink(1, 'Note A');
insertLink(2, 'Note B');
const result = generateDeletionCleanupSuggestions(db, [
'dir/Note A.md',
'dir/Note B.md',
]);
expect(result).toBeDefined();
expect(result!.broken_links).toHaveLength(2);
});
it('deduplicates broken link sources', () => {
insertNote('source.md', 'Source');
// Same source links to two deleted notes
insertLink(1, 'Note A');
insertLink(1, 'Note B');
const result = generateDeletionCleanupSuggestions(db, [
'dir/Note A.md',
'dir/Note B.md',
]);
expect(result).toBeDefined();
// source.md appears only once despite linking to both
expect(result!.broken_links).toHaveLength(1);
expect(result!.broken_links![0]).toBe('source.md');
});
});
});