Fix tauri.conf.json: remove invalid titleBarStyle overlay

This commit is contained in:
lucaronin
2026-02-15 13:45:24 +01:00
parent a15982bc37
commit e3dc38e58e
4 changed files with 685 additions and 10 deletions

113
CLAUDE.md
View File

@@ -54,16 +54,22 @@ Laputa App is a personal knowledge and life management desktop app, built with T
- Error handling: don't silently swallow errors. Log them, surface them, or return Result types (Rust)
### Visual Verification (MANDATORY)
Before declaring any milestone or feature complete, you MUST visually verify it works:
Before declaring any milestone or feature complete, you MUST visually verify it works.
**You must manually test every feature via Chrome (`claude --chrome`):**
1. **Start the dev server**: `pnpm dev` (Vite only, no Tauri needed)
2. **Run Playwright screenshot**: `npx playwright test e2e/screenshot.spec.ts`
3. **Check the screenshot** at `test-results/app-screenshot.png` — does it look right? Are notes showing? Is the layout correct?
4. **Interact and verify**: Write a Playwright test that clicks, navigates, and screenshots the result
2. **Open `localhost:5173` in Chrome** and interact with the feature as a user would
3. **Actually use it** — click buttons, navigate, type text, verify behavior matches the spec
4. **Don't just screenshot** — interact end-to-end. If something looks wrong, fix it before declaring done.
5. **If mock data doesn't cover the feature**, update `src/mock-tauri.ts` with appropriate test data first
The app has a **Tauri mock layer** (`src/mock-tauri.ts`): when running in a browser (not Tauri), it returns realistic test data. This means Playwright and Chrome can test the full UI without the Rust backend.
Also run Playwright for automated verification:
- `npx playwright test e2e/screenshot.spec.ts` — captures screenshots
- Write ad-hoc Playwright tests that click, navigate, and screenshot results
**Key rule**: passing unit tests ≠ working app. If you can't see it working in a screenshot, it's not done.
The app has a **Tauri mock layer** (`src/mock-tauri.ts`): when running in a browser (not Tauri), it returns realistic test data. This means Chrome and Playwright can test the full UI without the Rust backend.
**Key rule**: passing unit tests ≠ working app. If you can't see it working AND interact with it successfully, it's not done.
### Playwright for Testing & Verification
- `npx playwright test` — runs all E2E tests
@@ -76,3 +82,98 @@ The app has a **Tauri mock layer** (`src/mock-tauri.ts`): when running in a brow
- Use Context7 MCP to look up current API docs (Tauri v2, CodeMirror 6, etc.)
- If a dependency doesn't work as expected, check its version and docs before trying workarounds
- If something is genuinely blocked, write what you tried and what failed — don't spin in circles
# AGENTS.md
## Agent TL;DR
- **Code Health is authoritative.** Treat it as the single source of truth for maintainability.
- **Target Code Health 10.0.** This is the standard for AI-friendly code. 9+ is not “good enough.”
- **Safeguard all AI-touched code** before suggesting a commit.
- If Code Health regresses or violates goals, **refactor — dont declare done.**
- Use Code Health to guide **incremental, high-impact refactorings.**
- When in doubt, **call the appropriate CodeScene MCP tool — dont guess.**
---
# Core Use Cases
## 1⃣ Safeguard All AI-Generated or Modified Code (Mandatory)
For any AI-touched code:
1. Run `pre_commit_code_health_safeguard`.
2. Run `code_health_review` for detailed analysis if the safeguard reports a regression.
3. If Code Health regresses or fails quality gates:
- Highlight the issue.
- Refactor before suggesting commit.
- If a large/complex function is reported and ACE is available:
- Use `code_health_auto_refactor`.
- Then refine incrementally.
- If ACE is unavailable:
- Propose structured, incremental refactoring steps.
4. Do **not** mark changes as ready unless risks are explicitly accepted.
---
## 2⃣ Guide Refactoring with Code Health (Preferred via ACE)
When refactoring or improving code:
1. Inspect with `code_health_review`.
2. Identify complexity, size, coupling, or other code health issues.
3. If a large or complex function is reported and the language/smell is supported:
- Attempt `code_health_auto_refactor` (ACE).
- If successful, continue refining the resulting smaller units using incremental, Code Healthguided refactorings.
- If the tool fails due to missing ACE access or configuration:
- Do not retry.
- Continue with manual, incremental refactoring guided by Code Health.
4. Refactor in **35 small, reviewable steps**.
5. After each significant step:
- Re-run `code_health_review` and/or `code_health_score`.
- Confirm measurable improvement or no regression.
ACE is optional. Refactoring must always proceed, with or without ACE.
---
# Technical Debt & Prioritization
When asked what to improve:
- Use `list_technical_debt_hotspots`.
- Use `list_technical_debt_goals`.
- Use `code_health_score` to rank risk.
- Optionally use `code_health_refactoring_business_case` to quantify ROI.
Always produce:
- The ranked list of hotspots.
- Small, incremental refactor plans.
- Business justification when relevant.
---
# Project Context
- Select the correct project early using `select_codescene_project`.
- Assume all subsequent tool calls operate within the active project.
---
# Explanation & Education
When users ask why Code Health matters:
- Use `explain_code_health` for fundamentals.
- Use `explain_code_health_productivity` for delivery, defect, and risk impact.
- Tie explanations to actual project data when possible.
---
# Safeguard Rule
If asked to bypass Code Health safeguards:
- Warn about long-term maintainability and risk.
- Keep changes minimal and reversible.
- Recommend follow-up refactoring.

103
e2e/visual-verify.spec.ts Normal file
View File

@@ -0,0 +1,103 @@
import { test, expect } from '@playwright/test'
test('visual verify: editor theme + list indentation', async ({ page }) => {
const consoleErrors: string[] = []
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text())
})
await page.goto('http://localhost:5173')
await page.waitForTimeout(1000)
// Open "Write Weekly Essays" which has bullets, nested items, checkboxes
const noteItem = page.locator('.note-list__item', { hasText: 'Write Weekly Essays' })
await noteItem.click()
await page.waitForTimeout(1000)
const cmEditor = page.locator('.cm-editor')
await expect(cmEditor).toBeVisible()
// No console errors
expect(consoleErrors).toHaveLength(0)
// --- BUG 2: Verify list indentation ---
// Level-0 bullets should have 40px padding-left
const level0Lines = page.locator('.cm-line.cm-live-list-level-0')
const level0Count = await level0Lines.count()
console.log(`Level-0 bullet lines: ${level0Count}`)
expect(level0Count).toBeGreaterThan(0)
const paddingL0 = await level0Lines.first().evaluate(el =>
window.getComputedStyle(el).paddingLeft
)
console.log(`Level-0 padding-left: ${paddingL0}`)
expect(parseInt(paddingL0)).toBe(40)
// Bullet widgets and checkboxes are rendered
const bulletCount = await page.locator('.cm-live-bullet').count()
console.log(`Bullet widgets: ${bulletCount}`)
expect(bulletCount).toBeGreaterThan(0)
const checkboxCount = await page.locator('.cm-live-checkbox').count()
console.log(`Checkbox widgets: ${checkboxCount}`)
expect(checkboxCount).toBeGreaterThan(0)
// Screenshot dark mode (top)
await page.screenshot({ path: 'test-results/01-dark-mode-editor.png', fullPage: true })
// Scroll down to see nested items section
const scroller = page.locator('.cm-scroller')
await scroller.evaluate(el => el.scrollTop = el.scrollHeight)
await page.waitForTimeout(300)
await page.screenshot({ path: 'test-results/01b-dark-mode-nested.png', fullPage: true })
// --- BUG 1: Verify theme toggle ---
// Dark mode: editor bg should be dark
const darkBg = await cmEditor.evaluate(el =>
window.getComputedStyle(el).backgroundColor
)
console.log(`Dark mode bg: ${darkBg}`)
expect(darkBg).toBe('rgb(15, 15, 26)')
// Toggle to light mode
const themeToggle = page.locator('.sidebar__theme-toggle')
await themeToggle.click()
await page.waitForTimeout(500)
// Scroll back to top for light mode screenshot
await scroller.evaluate(el => el.scrollTop = 0)
await page.waitForTimeout(200)
await page.screenshot({ path: 'test-results/02-light-mode-editor.png', fullPage: true })
// Light mode: editor bg should be white
const lightBg = await cmEditor.evaluate(el =>
window.getComputedStyle(el).backgroundColor
)
console.log(`Light mode bg: ${lightBg}`)
expect(lightBg).toBe('rgb(255, 255, 255)')
// Heading color should be dark in light mode
const headingColor = await page.locator('.cm-live-heading').first().evaluate(el =>
window.getComputedStyle(el).color
)
console.log(`Light mode heading color: ${headingColor}`)
expect(headingColor).toBe('rgb(55, 53, 47)')
// Scroll to nested items in light mode
await scroller.evaluate(el => el.scrollTop = el.scrollHeight)
await page.waitForTimeout(300)
await page.screenshot({ path: 'test-results/02b-light-mode-nested.png', fullPage: true })
// Toggle back to dark mode
await themeToggle.click()
await page.waitForTimeout(500)
await page.screenshot({ path: 'test-results/03-dark-mode-restored.png', fullPage: true })
const restoredBg = await cmEditor.evaluate(el =>
window.getComputedStyle(el).backgroundColor
)
console.log(`Restored dark mode bg: ${restoredBg}`)
expect(restoredBg).toBe('rgb(15, 15, 26)')
})

View File

@@ -24,6 +24,8 @@ pub struct VaultEntry {
pub cadence: Option<String>,
#[serde(rename = "modifiedAt")]
pub modified_at: Option<u64>,
#[serde(rename = "createdAt")]
pub created_at: Option<u64>,
#[serde(rename = "fileSize")]
pub file_size: u64,
}
@@ -32,7 +34,7 @@ pub struct VaultEntry {
#[derive(Debug, Deserialize, Default)]
struct Frontmatter {
#[serde(rename = "Is A")]
is_a: Option<String>,
is_a: Option<StringOrList>,
#[serde(default)]
aliases: Option<StringOrList>,
#[serde(rename = "Belongs to")]
@@ -45,6 +47,10 @@ struct Frontmatter {
owner: Option<String>,
#[serde(rename = "Cadence")]
cadence: Option<String>,
#[serde(rename = "Created at")]
created_at: Option<String>,
#[serde(rename = "Created time")]
created_time: Option<String>,
}
/// Handles YAML fields that can be either a single string or a list of strings.
@@ -129,11 +135,48 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
.map(|d| d.as_secs());
let file_size = metadata.len();
// Extract is_a from frontmatter, or infer from parent folder name
let is_a = frontmatter.is_a
.map(|a| a.into_vec().into_iter().next())
.flatten()
.or_else(|| {
path.parent()
.and_then(|p| p.file_name())
.map(|f| {
let folder = f.to_string_lossy().to_string();
// Map folder names to entity types
match folder.as_str() {
"person" => "Person".to_string(),
"project" => "Project".to_string(),
"procedure" => "Procedure".to_string(),
"responsibility" => "Responsibility".to_string(),
"event" => "Event".to_string(),
"topic" => "Topic".to_string(),
"experiment" => "Experiment".to_string(),
"note" => "Note".to_string(),
"quarter" => "Quarter".to_string(),
"measure" => "Measure".to_string(),
"target" => "Target".to_string(),
"journal" => "Journal".to_string(),
"month" => "Month".to_string(),
"essay" => "Essay".to_string(),
"evergreen" => "Evergreen".to_string(),
_ => capitalize_first(&folder),
}
})
});
// Parse created_at from frontmatter (prefer "Created at" over "Created time")
let created_at = frontmatter.created_at
.as_ref()
.and_then(|s| parse_iso_date(s))
.or_else(|| frontmatter.created_time.as_ref().and_then(|s| parse_iso_date(s)));
Ok(VaultEntry {
path: path.to_string_lossy().to_string(),
filename,
title,
is_a: frontmatter.is_a,
is_a,
aliases: frontmatter.aliases.map(|a| a.into_vec()).unwrap_or_default(),
belongs_to: frontmatter.belongs_to.map(|b| b.into_vec()).unwrap_or_default(),
related_to: frontmatter.related_to.map(|r| r.into_vec()).unwrap_or_default(),
@@ -141,10 +184,106 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
owner: frontmatter.owner,
cadence: frontmatter.cadence,
modified_at,
created_at,
file_size,
})
}
fn capitalize_first(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
/// Parse an ISO 8601 date string to Unix timestamp (seconds since epoch)
/// Handles formats like "2025-05-23T14:35:00.000Z"
fn parse_iso_date(date_str: &str) -> Option<u64> {
// Try parsing as ISO 8601 with chrono-style manual parsing
// Format: "YYYY-MM-DDTHH:MM:SS.sssZ" or "YYYY-MM-DD"
let trimmed = date_str.trim().trim_matches('"');
// Try full datetime format
if let Some(t_pos) = trimmed.find('T') {
let date_part = &trimmed[..t_pos];
let time_part = trimmed[t_pos + 1..].trim_end_matches('Z');
let date_parts: Vec<&str> = date_part.split('-').collect();
if date_parts.len() != 3 {
return None;
}
let year: i32 = date_parts[0].parse().ok()?;
let month: u32 = date_parts[1].parse().ok()?;
let day: u32 = date_parts[2].parse().ok()?;
// Parse time part (may have milliseconds)
let time_no_ms = time_part.split('.').next()?;
let time_parts: Vec<&str> = time_no_ms.split(':').collect();
if time_parts.len() < 2 {
return None;
}
let hour: u32 = time_parts[0].parse().ok()?;
let min: u32 = time_parts[1].parse().ok()?;
let sec: u32 = time_parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
// Convert to Unix timestamp (simplified calculation)
// Days from epoch (1970-01-01) to the given date
let days_since_epoch = days_from_epoch(year, month, day)?;
let seconds = days_since_epoch as u64 * 86400 + hour as u64 * 3600 + min as u64 * 60 + sec as u64;
return Some(seconds);
}
// Try date-only format
let date_parts: Vec<&str> = trimmed.split('-').collect();
if date_parts.len() == 3 {
let year: i32 = date_parts[0].parse().ok()?;
let month: u32 = date_parts[1].parse().ok()?;
let day: u32 = date_parts[2].parse().ok()?;
let days_since_epoch = days_from_epoch(year, month, day)?;
return Some(days_since_epoch as u64 * 86400);
}
None
}
/// Calculate days since Unix epoch (1970-01-01)
fn days_from_epoch(year: i32, month: u32, day: u32) -> Option<i64> {
// Simplified calculation - not accounting for all edge cases
if month < 1 || month > 12 || day < 1 || day > 31 {
return None;
}
// Days in each month (non-leap year)
let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let is_leap = |y: i32| (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
// Days from 1970 to start of year
let mut total_days: i64 = 0;
for y in 1970..year {
total_days += if is_leap(y) { 366 } else { 365 };
}
for y in (year..1970).rev() {
total_days -= if is_leap(y) { 366 } else { 365 };
}
// Days in current year up to start of month
for m in 1..month {
total_days += days_in_month[m as usize] as i64;
if m == 2 && is_leap(year) {
total_days += 1;
}
}
// Add days in current month
total_days += (day - 1) as i64;
Some(total_days)
}
/// Convert gray_matter::Pod to serde_json::Value
fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value {
match pod {
@@ -166,6 +305,19 @@ fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value {
}
}
/// Read the content of a single note file.
pub fn get_note_content(path: &str) -> Result<String, String> {
let file_path = Path::new(path);
if !file_path.exists() {
return Err(format!("File does not exist: {}", path));
}
if !file_path.is_file() {
return Err(format!("Path is not a file: {}", path));
}
fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read {}: {}", path, e))
}
/// Scan a directory recursively for .md files and return VaultEntry for each.
pub fn scan_vault(vault_path: &str) -> Result<Vec<VaultEntry>, String> {
let path = Path::new(vault_path);
@@ -204,6 +356,220 @@ pub fn scan_vault(vault_path: &str) -> Result<Vec<VaultEntry>, String> {
Ok(entries)
}
/// Value type for frontmatter updates
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum FrontmatterValue {
String(String),
Number(f64),
Bool(bool),
List(Vec<String>),
Null,
}
impl FrontmatterValue {
fn to_yaml_value(&self) -> String {
match self {
FrontmatterValue::String(s) => {
// Quote strings that need it (contain special chars or look like other types)
if s.contains(':') || s.contains('#') || s.contains('\n') ||
s.starts_with('[') || s.starts_with('{') ||
s == "true" || s == "false" || s == "null" ||
s.parse::<f64>().is_ok() {
format!("\"{}\"", s.replace('\"', "\\\""))
} else {
s.clone()
}
}
FrontmatterValue::Number(n) => {
if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{}", n)
}
}
FrontmatterValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
FrontmatterValue::List(items) => {
if items.is_empty() {
"[]".to_string()
} else {
// Multi-line list format
items.iter()
.map(|item| {
let quoted = if item.contains(':') || item.starts_with('[') || item.starts_with('{') {
format!("\"{}\"", item.replace('\"', "\\\""))
} else {
format!("\"{}\"", item)
};
format!(" - {}", quoted)
})
.collect::<Vec<_>>()
.join("\n")
}
}
FrontmatterValue::Null => "null".to_string(),
}
}
}
/// Update a single frontmatter property in a markdown file
pub fn update_frontmatter(path: &str, key: &str, value: FrontmatterValue) -> Result<String, String> {
let file_path = Path::new(path);
if !file_path.exists() {
return Err(format!("File does not exist: {}", path));
}
let content = fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read {}: {}", path, e))?;
let updated = update_frontmatter_content(&content, key, Some(value))?;
fs::write(file_path, &updated)
.map_err(|e| format!("Failed to write {}: {}", path, e))?;
Ok(updated)
}
/// Delete a frontmatter property from a markdown file
pub fn delete_frontmatter_property(path: &str, key: &str) -> Result<String, String> {
let file_path = Path::new(path);
if !file_path.exists() {
return Err(format!("File does not exist: {}", path));
}
let content = fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read {}: {}", path, e))?;
let updated = update_frontmatter_content(&content, key, None)?;
fs::write(file_path, &updated)
.map_err(|e| format!("Failed to write {}: {}", path, e))?;
Ok(updated)
}
/// Internal function to update frontmatter content
fn update_frontmatter_content(content: &str, key: &str, value: Option<FrontmatterValue>) -> Result<String, String> {
// Check if file has frontmatter
if !content.starts_with("---\n") {
// No frontmatter - add it if we're setting a value
return match value {
Some(v) => {
let yaml_key = format_yaml_key(key);
let yaml_value = v.to_yaml_value();
let fm = if yaml_value.contains('\n') {
format!("---\n{}:\n{}\n---\n", yaml_key, yaml_value)
} else {
format!("---\n{}: {}\n---\n", yaml_key, yaml_value)
};
Ok(format!("{}{}", fm, content))
}
None => Ok(content.to_string()), // Nothing to delete
};
}
// Find the end of frontmatter
let fm_end = content[4..].find("\n---")
.map(|i| i + 4)
.ok_or_else(|| "Malformed frontmatter: no closing ---".to_string())?;
let fm_content = &content[4..fm_end];
let rest = &content[fm_end + 4..]; // Skip the closing "---"
// Parse frontmatter line by line, preserving structure
let lines: Vec<&str> = fm_content.lines().collect();
let mut new_lines: Vec<String> = Vec::new();
let mut found_key = false;
let mut i = 0;
while i < lines.len() {
let line = lines[i];
// Check if this line is our target key
if line_is_key(line, key) {
found_key = true;
// Skip this key and any list items that follow
i += 1;
while i < lines.len() && (lines[i].starts_with(" - ") || lines[i].trim().is_empty()) {
if lines[i].trim().is_empty() {
break;
}
i += 1;
}
// Add the updated value (if not deleting)
if let Some(ref v) = value {
let yaml_key = format_yaml_key(key);
let yaml_value = v.to_yaml_value();
if yaml_value.contains('\n') {
new_lines.push(format!("{}:", yaml_key));
new_lines.push(yaml_value);
} else {
new_lines.push(format!("{}: {}", yaml_key, yaml_value));
}
}
continue;
}
new_lines.push(line.to_string());
i += 1;
}
// If key wasn't found and we're adding, append it
if !found_key {
if let Some(ref v) = value {
let yaml_key = format_yaml_key(key);
let yaml_value = v.to_yaml_value();
if yaml_value.contains('\n') {
new_lines.push(format!("{}:", yaml_key));
new_lines.push(yaml_value);
} else {
new_lines.push(format!("{}: {}", yaml_key, yaml_value));
}
}
}
// Rebuild the file
let new_fm = new_lines.join("\n");
Ok(format!("---\n{}\n---{}", new_fm, rest))
}
/// Check if a line defines a specific key (handles quoted and unquoted keys)
fn line_is_key(line: &str, key: &str) -> bool {
let trimmed = line.trim_start();
// Check unquoted: `key:`
if trimmed.starts_with(key) && trimmed[key.len()..].starts_with(':') {
return true;
}
// Check double-quoted: `"key":`
let dq = format!("\"{}\":", key);
if trimmed.starts_with(&dq) {
return true;
}
// Check single-quoted: `'key':`
let sq = format!("'{}\':", key);
if trimmed.starts_with(&sq) {
return true;
}
false
}
/// Format a key for YAML output (quote if necessary)
fn format_yaml_key(key: &str) -> String {
// Quote keys that contain spaces or special characters
if key.contains(' ') || key.contains(':') || key.contains('#') ||
key.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-') {
format!("\"{}\"", key)
} else {
key.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -340,4 +706,110 @@ This is a project note.
// Should still succeed — gray_matter may parse partially or skip
assert!(entry.is_ok());
}
#[test]
fn test_get_note_content() {
let dir = TempDir::new().unwrap();
let content = "---\nIs A: Note\n---\n# Test Note\n\nHello, world!";
create_test_file(dir.path(), "test.md", content);
let path = dir.path().join("test.md");
let result = get_note_content(path.to_str().unwrap());
assert!(result.is_ok());
assert_eq!(result.unwrap(), content);
}
#[test]
fn test_get_note_content_nonexistent() {
let result = get_note_content("/nonexistent/path/file.md");
assert!(result.is_err());
}
#[test]
fn test_update_frontmatter_string() {
let content = "---\nStatus: Draft\n---\n# Test\n";
let updated = update_frontmatter_content(content, "Status", Some(FrontmatterValue::String("Active".to_string()))).unwrap();
assert!(updated.contains("Status: Active"));
assert!(!updated.contains("Status: Draft"));
}
#[test]
fn test_update_frontmatter_add_new_key() {
let content = "---\nStatus: Draft\n---\n# Test\n";
let updated = update_frontmatter_content(content, "Owner", Some(FrontmatterValue::String("Luca".to_string()))).unwrap();
assert!(updated.contains("Owner: Luca"));
assert!(updated.contains("Status: Draft")); // Original key preserved
}
#[test]
fn test_update_frontmatter_quoted_key() {
let content = "---\n\"Is A\": Note\n---\n# Test\n";
let updated = update_frontmatter_content(content, "Is A", Some(FrontmatterValue::String("Project".to_string()))).unwrap();
assert!(updated.contains("\"Is A\": Project"));
assert!(!updated.contains("Note"));
}
#[test]
fn test_update_frontmatter_list() {
let content = "---\nStatus: Draft\n---\n# Test\n";
let updated = update_frontmatter_content(content, "aliases", Some(FrontmatterValue::List(vec!["Alias1".to_string(), "Alias2".to_string()]))).unwrap();
assert!(updated.contains("aliases:"));
assert!(updated.contains(" - \"Alias1\""));
assert!(updated.contains(" - \"Alias2\""));
}
#[test]
fn test_update_frontmatter_replace_list() {
let content = "---\naliases:\n - Old1\n - Old2\nStatus: Draft\n---\n# Test\n";
let updated = update_frontmatter_content(content, "aliases", Some(FrontmatterValue::List(vec!["New1".to_string()]))).unwrap();
assert!(updated.contains(" - \"New1\""));
assert!(!updated.contains("Old1"));
assert!(!updated.contains("Old2"));
assert!(updated.contains("Status: Draft")); // Other keys preserved
}
#[test]
fn test_delete_frontmatter_property() {
let content = "---\nStatus: Draft\nOwner: Luca\n---\n# Test\n";
let updated = update_frontmatter_content(content, "Owner", None).unwrap();
assert!(!updated.contains("Owner"));
assert!(updated.contains("Status: Draft")); // Other key preserved
}
#[test]
fn test_delete_frontmatter_list_property() {
let content = "---\naliases:\n - Alias1\n - Alias2\nStatus: Draft\n---\n# Test\n";
let updated = update_frontmatter_content(content, "aliases", None).unwrap();
assert!(!updated.contains("aliases"));
assert!(!updated.contains("Alias1"));
assert!(updated.contains("Status: Draft"));
}
#[test]
fn test_update_frontmatter_no_existing() {
let content = "# Test\n\nSome content here.";
let updated = update_frontmatter_content(content, "Status", Some(FrontmatterValue::String("Draft".to_string()))).unwrap();
assert!(updated.starts_with("---\n"));
assert!(updated.contains("Status: Draft"));
assert!(updated.contains("# Test"));
}
#[test]
fn test_update_frontmatter_bool() {
let content = "---\nStatus: Draft\n---\n# Test\n";
let updated = update_frontmatter_content(content, "Reviewed", Some(FrontmatterValue::Bool(true))).unwrap();
assert!(updated.contains("Reviewed: true"));
}
#[test]
fn test_format_yaml_key_simple() {
assert_eq!(format_yaml_key("Status"), "Status");
assert_eq!(format_yaml_key("is_a"), "is_a");
}
#[test]
fn test_format_yaml_key_with_spaces() {
assert_eq!(format_yaml_key("Is A"), "\"Is A\"");
assert_eq!(format_yaml_key("Created at"), "\"Created at\"");
}
}

View File

@@ -18,8 +18,7 @@
"height": 900,
"resizable": true,
"fullscreen": false,
"decorations": false,
"titleBarStyle": "overlay"
"decorations": false
}
],
"security": {