From feef938174f97267aa2a8b7d5bec57bddb659065 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 19 Apr 2026 01:33:53 +0200 Subject: [PATCH] fix: align fresh vault seed files --- src-tauri/src/commands/vault.rs | 55 ++++++++++++++------ src-tauri/src/lib.rs | 2 +- src-tauri/src/vault/config_seed.rs | 51 ++++++++++++------ src-tauri/src/vault/getting_started.rs | 8 +-- tests/smoke/create-empty-vault-flows.spec.ts | 43 ++++++++++++--- 5 files changed, 116 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/commands/vault.rs b/src-tauri/src/commands/vault.rs index 6965d034..fa8f3b38 100644 --- a/src-tauri/src/commands/vault.rs +++ b/src-tauri/src/commands/vault.rs @@ -314,6 +314,7 @@ pub fn repair_vault(vault_path: String) -> Result { #[cfg(test)] mod tests { use super::*; + use std::path::Path; fn temp_note(body: &str) -> (tempfile::TempDir, std::path::PathBuf) { let dir = tempfile::TempDir::new().unwrap(); @@ -322,6 +323,35 @@ mod tests { (dir, note) } + fn assert_paths_exist(root: &Path, paths: &[&str]) { + for path in paths { + assert!(root.join(path).exists(), "{path} should exist"); + } + } + + fn assert_paths_absent(root: &Path, paths: &[&str]) { + for path in paths { + assert!(!root.join(path).exists(), "{path} should be absent"); + } + } + + fn assert_seeded_guidance_content(vault_path: &Path) { + let agents = std::fs::read_to_string(vault_path.join("AGENTS.md")).unwrap(); + let claude = std::fs::read_to_string(vault_path.join("CLAUDE.md")).unwrap(); + + assert!(agents.contains("Legacy `title:` frontmatter is still read as a fallback")); + assert!(agents.contains("views/*.yml")); + assert!(claude.starts_with("@AGENTS.md")); + assert!(claude.contains("# CLAUDE.md")); + } + + fn assert_seeded_type_scaffolding(vault_path: &Path) { + let type_definition = std::fs::read_to_string(vault_path.join("type.md")).unwrap(); + + assert!(type_definition.contains("visible: false")); + assert!(type_definition.contains("# Type")); + } + #[test] fn test_batch_archive_notes() { let (_dir, note) = temp_note("---\nStatus: Active\n---\n# Note\n"); @@ -450,27 +480,21 @@ mod tests { let result = repair_vault(vault_path.to_str().unwrap().to_string()); assert!(result.is_ok()); - assert!(vault_path.join("AGENTS.md").exists()); - assert!(vault_path.join("config.md").exists()); - assert!(vault_path.join(".gitignore").exists()); + assert_paths_exist(vault_path, &["AGENTS.md", "CLAUDE.md", "type.md", "note.md", ".gitignore"]); + assert_paths_absent(vault_path, &["config.md"]); } #[test] - fn test_create_empty_vault_seeds_agents_and_config() { + fn test_create_empty_vault_seeds_agents_and_type_scaffolding() { let dir = tempfile::TempDir::new().unwrap(); let vault_path = dir.path().join("fresh-vault"); let result = create_empty_vault(vault_path.to_string_lossy().to_string()); assert!(result.is_ok()); - assert!(vault_path.join(".git").exists()); - assert!(vault_path.join("AGENTS.md").exists()); - assert!(vault_path.join("CLAUDE.md").exists()); - assert!(vault_path.join("config.md").exists()); - assert!(vault_path.join("note.md").exists()); - - let agents = std::fs::read_to_string(vault_path.join("AGENTS.md")).unwrap(); - assert!(agents.contains("Legacy `title:` frontmatter is still read as a fallback")); - assert!(agents.contains("views/*.yml")); + assert_paths_exist(&vault_path, &[".git", "AGENTS.md", "CLAUDE.md", "type.md", "note.md"]); + assert_paths_absent(&vault_path, &["config.md"]); + assert_seeded_guidance_content(&vault_path); + assert_seeded_type_scaffolding(&vault_path); } #[test] @@ -484,8 +508,7 @@ mod tests { let err = result.expect_err("expected non-empty folder to be rejected"); assert_eq!(err, "Choose an empty folder to create a new vault"); - assert!(vault_path.join("keep.txt").exists()); - assert!(!vault_path.join(".git").exists()); - assert!(!vault_path.join("AGENTS.md").exists()); + assert_paths_exist(&vault_path, &["keep.txt"]); + assert_paths_absent(&vault_path, &[".git", "AGENTS.md"]); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 45b42f7a..2bffa7ee 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,7 +46,7 @@ fn run_startup_tasks() { ); // Migrate legacy config/agents.md → root AGENTS.md (one-time, idempotent) vault::migrate_agents_md(vp_str); - // Seed AGENTS.md and config.md at vault root if missing + // Seed AGENTS.md and starter type definitions at vault root if missing vault::seed_config_files(vp_str); // Register Tolaria MCP server in Claude Code and Cursor configs diff --git a/src-tauri/src/vault/config_seed.rs b/src-tauri/src/vault/config_seed.rs index e6cdd17c..7f95de2c 100644 --- a/src-tauri/src/vault/config_seed.rs +++ b/src-tauri/src/vault/config_seed.rs @@ -4,19 +4,25 @@ use std::path::{Path, PathBuf}; use super::getting_started::{agents_content_can_be_refreshed, AGENTS_MD}; -/// Content for `config.md` — gives the Config type a sidebar icon and label. -const CONFIG_TYPE_DEFINITION: &str = "\ +/// Content for `type.md` — describes the generic Type metamodel for the vault. +const TYPE_TYPE_DEFINITION: &str = "\ --- type: Type -icon: gear-six -color: gray -order: 90 -sidebar label: Config +order: 0 +visible: false --- -# Config +# Type -Vault configuration files. These control how AI agents, tools, and other integrations interact with this vault. +A Type defines shared metadata and defaults for a category of notes in this vault. + +## Common properties +- **Icon**: Sidebar icon for this type +- **Color**: Accent color for notes of this type +- **Order**: Sidebar ordering +- **Sidebar label**: Override the default plural label +- **Template**: Default body for new notes of this type +- **View**: Preferred note-list view for this type "; /// Content for `note.md` — restores the default Note type definition when missing. @@ -30,8 +36,15 @@ type: Type A Note is a general-purpose document — research notes, meeting notes, strategy docs, or anything that doesn't fit a more specific type. "; +const LEGACY_CLAUDE_MD_SHIM: &str = "@AGENTS.md + +This file is a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`. +"; + const CLAUDE_MD_SHIM: &str = "@AGENTS.md +# CLAUDE.md + This file is a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`. "; @@ -80,7 +93,9 @@ fn root_agents_can_be_replaced(path: &Path) -> bool { fn matches_claude_shim(content: &str) -> bool { let trimmed = content.trim(); - trimmed == "@AGENTS.md" || trimmed == CLAUDE_MD_SHIM.trim() + trimmed == "@AGENTS.md" + || trimmed == LEGACY_CLAUDE_MD_SHIM.trim() + || trimmed == CLAUDE_MD_SHIM.trim() } fn claude_shim_can_be_replaced(path: &Path) -> bool { @@ -251,7 +266,7 @@ fn ensure_root_type_definition(vault_path: &Path, file_name: &str, content: &str /// Ensure the default root type definitions exist for opened/repaired vaults. fn ensure_root_type_definitions(vault_path: &Path) { - ensure_root_type_definition(vault_path, "config.md", CONFIG_TYPE_DEFINITION); + ensure_root_type_definition(vault_path, "type.md", TYPE_TYPE_DEFINITION); ensure_root_type_definition(vault_path, "note.md", NOTE_TYPE_DEFINITION); } @@ -296,7 +311,7 @@ pub fn repair_config_files(vault_path: impl AsRef) -> Result { const gettingStartedPath = '/Users/mock/Documents/Getting Started' - function buildEntry(vaultPath: string, title: string): MockEntry { - const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + function buildEntry(vaultPath: string, title: string, options: EntryOptions = {}): MockEntry { + const defaultStem = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + const filename = options.fileName ?? `${defaultStem}.md` return { - path: `${vaultPath}/${slug}.md`, - filename: `${slug}.md`, + path: `${vaultPath}/${filename}`, + filename, title, - isA: 'Note', + isA: options.isA ?? 'Note', aliases: [], belongsTo: [], relatedTo: [], @@ -59,7 +72,7 @@ async function installEmptyVaultMocks( modifiedAt: 1700000000, createdAt: null, fileSize: 256, - snippet: `${title} snippet`, + snippet: options.snippet ?? `${title} snippet`, wordCount: 12, relationships: {}, outgoingLinks: [], @@ -69,6 +82,19 @@ async function installEmptyVaultMocks( } } + function buildFreshVaultSeedEntries(vaultPath: string): MockEntry[] { + return [ + buildEntry(vaultPath, 'AGENTS.md — Tolaria Vault', { fileName: 'AGENTS.md' }), + buildEntry(vaultPath, 'CLAUDE.md', { fileName: 'CLAUDE.md' }), + buildEntry(vaultPath, 'Type', { fileName: 'type.md', isA: 'Type' }), + buildEntry(vaultPath, 'Note', { fileName: 'note.md', isA: 'Type' }), + ] + } + + function syncEntryContent(entry: MockEntry) { + allContent[entry.path] = `# ${entry.title}\n\n${entry.snippet}` + } + let savedVaults = mockConfig.initialVaults.map(({ label, path }) => ({ label, path })) let activeVault = mockConfig.activeVault let hiddenDefaults: string[] = [] @@ -125,7 +151,8 @@ async function installEmptyVaultMocks( if (args.targetPath !== mockConfig.createdVaultPath) { throw new Error(`Unexpected empty vault target: ${args.targetPath}`) } - entriesByVault[mockConfig.createdVaultPath] ??= [] + entriesByVault[mockConfig.createdVaultPath] = buildFreshVaultSeedEntries(mockConfig.createdVaultPath) + entriesByVault[mockConfig.createdVaultPath].forEach(syncEntryContent) return mockConfig.createdVaultPath } ref.list_vault = (args: { path?: string }) => entriesByVault[args.path ?? activeVault ?? ''] ?? [] @@ -159,6 +186,7 @@ test('keyboard onboarding can create an empty vault and the first note', async ( await page.keyboard.press('Enter') await expect(page.getByTestId('note-list-container')).toBeVisible({ timeout: 5_000 }) + await expectFreshVaultSeedEntries(page) await sendShortcut(page, 'n', ['Control']) await expect(untitledNoteRow(page)).toBeVisible({ timeout: 5_000 }) }) @@ -190,6 +218,7 @@ test('command palette and bottom bar expose empty-vault creation from the active await page.keyboard.press('Enter') await expect(trigger).toContainText('Client Vault') + await expectFreshVaultSeedEntries(page) await sendShortcut(page, 'n', ['Control']) await expect(untitledNoteRow(page)).toBeVisible({ timeout: 5_000 }) })