fix: repair vault now flattens type folders and migrates legacy frontmatter
Repair Vault command now runs flatten_vault() and migrate_is_a_to_type() before restoring themes and config, ensuring vaults adopt flat structure. Also fixes config.md type definition to use `type: Type` instead of legacy `Is A: Type`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -673,7 +673,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|
||||
| `create_vault_theme` | Create markdown theme note |
|
||||
| `ensure_vault_themes` | Seed default themes if missing |
|
||||
| `restore_default_themes` | Restore all default themes |
|
||||
| `repair_vault` | Restore default themes + missing config files |
|
||||
| `repair_vault` | Flatten vault structure, migrate legacy frontmatter, restore themes + config |
|
||||
|
||||
### AI & MCP
|
||||
|
||||
|
||||
@@ -552,6 +552,10 @@ pub fn restore_default_themes(vault_path: String) -> Result<String, String> {
|
||||
#[tauri::command]
|
||||
pub fn repair_vault(vault_path: String) -> Result<String, String> {
|
||||
let vault_path = expand_tilde(&vault_path);
|
||||
// Migrate legacy is_a/Is A frontmatter → type
|
||||
vault::migrate_is_a_to_type(&vault_path)?;
|
||||
// Flatten vault: move notes from type-based subfolders to root
|
||||
vault::flatten_vault(&vault_path)?;
|
||||
// Migrate legacy theme/ directory to root, then repair themes
|
||||
theme::migrate_theme_dir_to_root(&vault_path);
|
||||
theme::restore_default_themes(&vault_path)?;
|
||||
@@ -788,4 +792,49 @@ mod tests {
|
||||
let result = get_default_vault_path();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repair_vault_flattens_type_folders() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let vault = dir.path();
|
||||
let note_dir = vault.join("note");
|
||||
std::fs::create_dir_all(¬e_dir).unwrap();
|
||||
std::fs::write(
|
||||
note_dir.join("hello.md"),
|
||||
"---\nis_a: Note\n---\n# Hello\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = repair_vault(vault.to_str().unwrap().to_string());
|
||||
assert!(result.is_ok());
|
||||
// Note moved from note/ subfolder to root
|
||||
assert!(vault.join("hello.md").exists());
|
||||
assert!(!note_dir.join("hello.md").exists());
|
||||
// Legacy is_a migrated to type
|
||||
let content = std::fs::read_to_string(vault.join("hello.md")).unwrap();
|
||||
assert!(content.contains("type: Note"));
|
||||
assert!(!content.contains("is_a:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repair_vault_creates_config_and_theme_files() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let vault = dir.path();
|
||||
|
||||
let result = repair_vault(vault.to_str().unwrap().to_string());
|
||||
assert!(result.is_ok());
|
||||
// Config files at root
|
||||
assert!(vault.join("AGENTS.md").exists());
|
||||
assert!(vault.join("config.md").exists());
|
||||
// Theme files at root (flat structure)
|
||||
assert!(vault.join("default-theme.md").exists());
|
||||
assert!(vault.join("dark-theme.md").exists());
|
||||
assert!(vault.join("minimal-theme.md").exists());
|
||||
assert!(vault.join("theme.md").exists());
|
||||
// No type/themes subfolders
|
||||
assert!(!vault.join("theme").exists());
|
||||
assert!(!vault.join("config").exists());
|
||||
// .gitignore
|
||||
assert!(vault.join(".gitignore").exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::getting_started::AGENTS_MD;
|
||||
/// Content for `config.md` — gives the Config type a sidebar icon and label.
|
||||
const CONFIG_TYPE_DEFINITION: &str = "\
|
||||
---
|
||||
Is A: Type
|
||||
type: Type
|
||||
icon: gear-six
|
||||
color: gray
|
||||
order: 90
|
||||
@@ -174,7 +174,7 @@ mod tests {
|
||||
|
||||
assert!(vault.join("config.md").exists());
|
||||
let content = fs::read_to_string(vault.join("config.md")).unwrap();
|
||||
assert!(content.contains("Is A: Type"));
|
||||
assert!(content.contains("type: Type"));
|
||||
assert!(content.contains("icon: gear-six"));
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction
|
||||
{ id: 'install-mcp', label: mcpStatus === 'installed' ? 'Restore MCP Server' : 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install', 'restore', 'fix', 'repair'], enabled: true, execute: () => onInstallMcp?.() },
|
||||
{ id: 'reindex-vault', label: 'Reindex Vault', group: 'Settings', keywords: ['reindex', 'index', 'search', 'rebuild', 'refresh'], enabled: !!onReindexVault, execute: () => onReindexVault?.() },
|
||||
{ id: 'reload-vault', label: 'Reload Vault', group: 'Settings', keywords: ['reload', 'refresh', 'rescan', 'sync', 'filesystem', 'cache'], enabled: !!onReloadVault, execute: () => onReloadVault?.() },
|
||||
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
|
||||
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset', 'flatten', 'structure'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
|
||||
|
||||
// Type-aware: "New [Type]" and "List [Type]"
|
||||
...buildTypeCommands(vaultTypes, onCreateNoteOfType, onSelect),
|
||||
|
||||
44
tests/smoke/repair-vault-flat-structure.spec.ts
Normal file
44
tests/smoke/repair-vault-flat-structure.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Repair Vault — flat structure', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
})
|
||||
|
||||
test('repair vault executes without errors', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text())
|
||||
})
|
||||
|
||||
// Open command palette and run Repair Vault
|
||||
await page.keyboard.press('Meta+k')
|
||||
await page.waitForTimeout(300)
|
||||
await page.keyboard.type('repair vault')
|
||||
await page.waitForTimeout(300)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// No console errors related to repair_vault
|
||||
const repairErrors = errors.filter(e => e.includes('repair_vault'))
|
||||
expect(repairErrors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('app loads without type folder structure errors', async ({ page }) => {
|
||||
const main = page.locator('#root')
|
||||
await expect(main).toBeVisible()
|
||||
|
||||
const errors: string[] = []
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text())
|
||||
})
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// No errors about move_note_to_type_folder or type folder operations
|
||||
const folderErrors = errors.filter(
|
||||
e => e.includes('move_note_to_type_folder') || e.includes('type_folder'),
|
||||
)
|
||||
expect(folderErrors).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user