fix: align fresh vault seed files
This commit is contained in:
@@ -314,6 +314,7 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
|
||||
#[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<str>) -> Result<String, String
|
||||
let _ = cleanup_empty_config_dir(vault)?;
|
||||
sync_ai_guidance_files(vault)?;
|
||||
|
||||
write_if_missing(&vault.join("config.md"), CONFIG_TYPE_DEFINITION)?;
|
||||
write_if_missing(&vault.join("type.md"), TYPE_TYPE_DEFINITION)?;
|
||||
write_if_missing(&vault.join("note.md"), NOTE_TYPE_DEFINITION)?;
|
||||
|
||||
Ok("Config files repaired".to_string())
|
||||
@@ -445,12 +460,13 @@ mod tests {
|
||||
|
||||
seed_config_files(vault.to_str().unwrap());
|
||||
|
||||
assert!(vault.join("config.md").exists());
|
||||
assert!(vault.join("type.md").exists());
|
||||
assert!(vault.join("note.md").exists());
|
||||
let config_content = fs::read_to_string(vault.join("config.md")).unwrap();
|
||||
let type_content = fs::read_to_string(vault.join("type.md")).unwrap();
|
||||
let note_content = fs::read_to_string(vault.join("note.md")).unwrap();
|
||||
assert!(config_content.contains("type: Type"));
|
||||
assert!(config_content.contains("icon: gear-six"));
|
||||
assert!(type_content.contains("type: Type"));
|
||||
assert!(type_content.contains("# Type"));
|
||||
assert!(type_content.contains("visible: false"));
|
||||
assert!(note_content.contains("type: Type"));
|
||||
assert!(note_content.contains("# Note"));
|
||||
assert!(!vault.join("config").exists());
|
||||
@@ -569,12 +585,15 @@ mod tests {
|
||||
|
||||
assert!(vault.join("AGENTS.md").exists());
|
||||
assert!(vault.join("CLAUDE.md").exists());
|
||||
assert!(vault.join("config.md").exists());
|
||||
assert!(vault.join("type.md").exists());
|
||||
assert!(vault.join("note.md").exists());
|
||||
assert!(!vault.join("config").exists());
|
||||
|
||||
let agents = read_root_agents(&vault);
|
||||
assert!(agents.contains("Tolaria Vault"));
|
||||
let type_content = fs::read_to_string(vault.join("type.md")).unwrap();
|
||||
assert!(type_content.contains("# Type"));
|
||||
assert!(type_content.contains("visible: false"));
|
||||
let note_content = fs::read_to_string(vault.join("note.md")).unwrap();
|
||||
assert!(note_content.contains("type: Type"));
|
||||
assert!(note_content.contains("general-purpose document"));
|
||||
|
||||
@@ -276,7 +276,7 @@ Keep edits compatible with this starter vault's current conventions. Prefer smal
|
||||
- One Markdown note per file.
|
||||
- The first H1 in the body is the preferred display title. Legacy `title:` frontmatter is still read as a fallback when a note has no H1, but do not add it to new notes.
|
||||
- Store note type in the `type:` frontmatter field.
|
||||
- In this starter vault, type definitions currently live at the vault root, for example `project.md`, `person.md`, `note.md`, and `config.md`. Keep new type files at the vault root unless the user explicitly asks to reorganize them.
|
||||
- In this starter vault, type definitions currently live at the vault root, for example `project.md`, `person.md`, `note.md`, and `type.md`. Keep new type files at the vault root unless the user explicitly asks to reorganize them.
|
||||
- Saved views live in `views/*.yml`.
|
||||
- Files in `attachments/` are assets, not notes. Reference them from notes, but do not treat them as notes or types.
|
||||
- Frontmatter properties that start with `_` are usually Tolaria-managed state. Leave them alone unless the user explicitly asks for them to change.
|
||||
@@ -649,7 +649,8 @@ mod tests {
|
||||
|
||||
let content = fs::read_to_string(dest.join("AGENTS.md")).unwrap();
|
||||
assert_eq!(content, AGENTS_MD);
|
||||
assert!(dest.join("config.md").exists());
|
||||
assert!(dest.join("type.md").exists());
|
||||
assert!(dest.join("note.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -686,7 +687,8 @@ mod tests {
|
||||
fs::read_to_string(dest.join("AGENTS.md")).unwrap(),
|
||||
AGENTS_MD
|
||||
);
|
||||
assert!(dest.join("config.md").exists());
|
||||
assert!(dest.join("type.md").exists());
|
||||
assert!(dest.join("note.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -29,10 +29,22 @@ interface VaultSeed {
|
||||
noteTitle?: string
|
||||
}
|
||||
|
||||
interface EntryOptions {
|
||||
fileName?: string
|
||||
isA?: string
|
||||
snippet?: string
|
||||
}
|
||||
|
||||
function untitledNoteRow(page: Page) {
|
||||
return page.getByText(/^Untitled Note(?: \d+)?$/i).first()
|
||||
}
|
||||
|
||||
async function expectFreshVaultSeedEntries(page: Page) {
|
||||
await expect(page.getByText('AGENTS.md — Tolaria Vault', { exact: true })).toBeVisible()
|
||||
await expect(page.getByText('CLAUDE.md', { exact: true })).toBeVisible()
|
||||
await expect(page.getByText('Config', { exact: true })).toHaveCount(0)
|
||||
}
|
||||
|
||||
async function installEmptyVaultMocks(
|
||||
page: Page,
|
||||
config: {
|
||||
@@ -44,13 +56,14 @@ async function installEmptyVaultMocks(
|
||||
await page.addInitScript((mockConfig) => {
|
||||
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 })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user