f0c51fbfcc
Extend the cursor-line insertion from :NuwikiTOC to the other buffer list generators: :NuwikiGenerateLinks and :NuwikiGenerateTagLinks. They appended at end-of-file; a fresh section now goes at the cursor line (with a leading blank), matching :NuwikiTOC. An existing section is still refreshed in place, and the auto_generate_*-on-save hooks are unaffected (they only ever replace). Shared section_insert() helper computes the insert position/block for all three (cursor line clamped to the document, else the per-command fallback: top of file for TOC, EOF for links/tags). Clients send the 0-based cursor line; handlers parse it via parse_uri_line_arg and thread it through links_edit / tag_links_edit. Rebuild variants pass None. Tests: links_edit_inserts_at_cursor_line + tag_links_edit_inserts_at_cursor_line. 577 passed, clippy clean, keymap harnesses green. Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
594 lines
21 KiB
Rust
594 lines
21 KiB
Rust
//! Command-coverage suite: at least one behavioural test for every
|
|
//! documented `:Nuwiki*` / `:Vimwiki*` command (see the COMMANDS section
|
|
//! of `doc/nuwiki.txt`).
|
|
//!
|
|
//! The user-facing commands funnel through `workspace/executeCommand`
|
|
//! into the Backend dispatcher, or — for follow/backlinks/rename — through
|
|
//! the standard LSP requests. The Backend itself owns a live `Client` and
|
|
//! can't be built in an integration test, so each case drives the *pure*
|
|
//! building-block the command relies on (the same functions the dispatcher
|
|
//! wraps). This file is organised to mirror the doc's command groups so a
|
|
//! reader can confirm every command has coverage.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use nuwiki_core::ast::{InlineNode, LinkKind, LinkTarget};
|
|
use nuwiki_core::date::DiaryDate;
|
|
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
|
use nuwiki_core::syntax::SyntaxPlugin;
|
|
use nuwiki_lsp::commands::{export_ops, ops, COMMANDS};
|
|
use nuwiki_lsp::config::WikiConfig;
|
|
use nuwiki_lsp::index::WorkspaceIndex;
|
|
use nuwiki_lsp::nav::find_inline_at;
|
|
use nuwiki_lsp::rename::{apply_links_space_char, build_new_uri, rewrite_wikilink_target};
|
|
use nuwiki_lsp::wiki::{build_wikis, resolve_uri_to_wiki};
|
|
use nuwiki_lsp::{diary, export};
|
|
use tower_lsp::lsp_types::Url;
|
|
|
|
fn parse(src: &str) -> nuwiki_core::ast::DocumentNode {
|
|
VimwikiSyntax::new().parse(src)
|
|
}
|
|
|
|
fn wiki_cfg(root: &str) -> WikiConfig {
|
|
WikiConfig::from_root(PathBuf::from(root))
|
|
}
|
|
|
|
/// Build a workspace index rooted at `root` with the given `name -> source`
|
|
/// pages, mirroring the helper used by the other command suites.
|
|
fn build_index(root: &str, pages: &[(&str, &str)]) -> WorkspaceIndex {
|
|
let mut idx = WorkspaceIndex::new(Some(PathBuf::from(root)));
|
|
for (name, src) in pages {
|
|
let uri = Url::from_file_path(format!("{root}/{name}.wiki")).unwrap();
|
|
idx.upsert(uri, &parse(src));
|
|
}
|
|
idx
|
|
}
|
|
|
|
fn diary_index(root: &str, dates: &[&str]) -> WorkspaceIndex {
|
|
let mut idx =
|
|
WorkspaceIndex::new(Some(PathBuf::from(root))).with_diary_rel_path(Some("diary".into()));
|
|
for d in dates {
|
|
let uri = Url::from_file_path(format!("{root}/diary/{d}.wiki")).unwrap();
|
|
idx.upsert(uri, &parse("= entry =\n"));
|
|
}
|
|
idx
|
|
}
|
|
|
|
// =====================================================================
|
|
// Group 1: Wiki / navigation
|
|
// =====================================================================
|
|
|
|
// :NuwikiIndex — open wiki N's index page.
|
|
#[test]
|
|
fn nuwiki_index_resolves_index_page() {
|
|
let idx = build_index("/wiki", &[("index", "= Home =\n"), ("Other", "= O =\n")]);
|
|
let page = idx.page_by_name("index").expect("index page indexed");
|
|
assert!(page.uri.as_str().ends_with("/wiki/index.wiki"));
|
|
}
|
|
|
|
// :NuwikiTabIndex — same target as :NuwikiIndex, opened in a new tab.
|
|
// The tab-vs-current split is an editor concern; the resolved target is
|
|
// identical, and both verbs are advertised as executeCommands.
|
|
#[test]
|
|
fn nuwiki_tab_index_advertised_alongside_index() {
|
|
assert!(COMMANDS.contains(&"nuwiki.wiki.openIndex"));
|
|
assert!(COMMANDS.contains(&"nuwiki.wiki.tabOpenIndex"));
|
|
}
|
|
|
|
// :NuwikiUISelect — pick a wiki from the configured list.
|
|
#[test]
|
|
fn nuwiki_ui_select_lists_configured_wikis() {
|
|
let cfgs = vec![wiki_cfg("/a/personal"), wiki_cfg("/b/work")];
|
|
let wikis = build_wikis(&cfgs);
|
|
assert_eq!(wikis.len(), 2);
|
|
assert_eq!(wikis[0].config.name, "personal");
|
|
assert_eq!(wikis[1].config.name, "work");
|
|
|
|
// Selection by URI picks the wiki that actually owns the file.
|
|
let uri = Url::from_file_path("/b/work/Page.wiki").unwrap();
|
|
assert_eq!(resolve_uri_to_wiki(&wikis, &uri), Some(wikis[1].id));
|
|
}
|
|
|
|
// :NuwikiGoto {page} — open `{page}.wiki` by name.
|
|
#[test]
|
|
fn nuwiki_goto_opens_page_by_name() {
|
|
let idx = build_index("/wiki", &[("index", "= H =\n"), ("Recipes", "= R =\n")]);
|
|
let page = idx
|
|
.page_by_name("Recipes")
|
|
.expect("page resolvable by name");
|
|
assert!(page.uri.as_str().ends_with("/wiki/Recipes.wiki"));
|
|
assert!(idx.page_by_name("Missing").is_none());
|
|
}
|
|
|
|
// :NuwikiFollowLink — follow the link under the cursor.
|
|
#[test]
|
|
fn nuwiki_follow_link_resolves_target_under_cursor() {
|
|
let idx = build_index(
|
|
"/wiki",
|
|
&[("Home", "see [[About]]\n"), ("About", "= A =\n")],
|
|
);
|
|
let doc = parse("see [[About]]\n");
|
|
// Byte col 8 sits inside "[[About]]" (starts at byte 4).
|
|
let node = find_inline_at(&doc, 0, 8).expect("wikilink under cursor");
|
|
let InlineNode::WikiLink(w) = node else {
|
|
panic!("expected a wikilink, got {node:?}");
|
|
};
|
|
let about = Url::from_file_path("/wiki/About.wiki").unwrap();
|
|
assert_eq!(idx.resolve(&w.target, "Home"), Some(&about));
|
|
}
|
|
|
|
// :NuwikiBacklinks — every reference to the current page.
|
|
#[test]
|
|
fn nuwiki_backlinks_lists_referrers() {
|
|
let idx = build_index(
|
|
"/wiki",
|
|
&[
|
|
("Home", "[[About]]\n"),
|
|
("Contact", "see [[About]] too\n"),
|
|
("About", "= A =\n"),
|
|
],
|
|
);
|
|
let back = idx.backlinks_for("About");
|
|
assert_eq!(back.len(), 2);
|
|
assert!(idx.backlinks_for("Home").is_empty());
|
|
}
|
|
|
|
// :NuwikiNextLink / :NuwikiPrevLink — jump to the next / previous wikilink.
|
|
// The jump itself is editor-side cursor movement; what the server provides
|
|
// is the ordered set of links on the page.
|
|
#[test]
|
|
fn nuwiki_next_prev_link_walk_links_in_document_order() {
|
|
let doc = parse("intro [[First]] mid [[Second]] end [[Third]]\n");
|
|
let links = ops::collect_wiki_links(&doc);
|
|
let targets: Vec<&str> = links
|
|
.iter()
|
|
.filter_map(|l| l.target.path.as_deref())
|
|
.collect();
|
|
assert_eq!(targets, vec!["First", "Second", "Third"]);
|
|
}
|
|
|
|
// :NuwikiBaddLink — add the target of the link under the cursor to the
|
|
// buffer list. The editor :badds whatever URI the link resolves to.
|
|
#[test]
|
|
fn nuwiki_badd_link_resolves_target_uri() {
|
|
let idx = build_index(
|
|
"/wiki",
|
|
&[("Home", "[[Notes/Todo]]\n"), ("Notes/Todo", "= T =\n")],
|
|
);
|
|
let target = LinkTarget {
|
|
kind: LinkKind::Wiki,
|
|
path: Some("Notes/Todo".into()),
|
|
..Default::default()
|
|
};
|
|
let expected = Url::from_file_path("/wiki/Notes/Todo.wiki").unwrap();
|
|
assert_eq!(idx.resolve(&target, "Home"), Some(&expected));
|
|
}
|
|
|
|
// :NuwikiRenameFile — rename the page and rewrite every inbound link.
|
|
#[test]
|
|
fn nuwiki_rename_file_builds_uri_and_rewrites_links() {
|
|
let new_uri = build_new_uri(&PathBuf::from("/wiki"), "New Name", ".wiki").unwrap();
|
|
assert!(new_uri.as_str().ends_with("/wiki/New%20Name.wiki"));
|
|
|
|
assert_eq!(
|
|
rewrite_wikilink_target("[[Old Name|caption]]", "New Name"),
|
|
Some("[[New Name|caption]]".to_string())
|
|
);
|
|
}
|
|
|
|
// links_space_char rewrites spaces in the link target / on-disk path while
|
|
// the default of a single space keeps the name verbatim.
|
|
#[test]
|
|
fn links_space_char_replaces_spaces_in_target_path() {
|
|
// Default " " — identity, spaces preserved.
|
|
assert_eq!(apply_links_space_char("My Page", " "), "My Page");
|
|
// Custom char — spaces become the configured glyph.
|
|
assert_eq!(apply_links_space_char("My Page", "_"), "My_Page");
|
|
// Subdir separators are untouched; only spaces within segments change.
|
|
assert_eq!(
|
|
apply_links_space_char("sub dir/My Page", "_"),
|
|
"sub_dir/My_Page"
|
|
);
|
|
|
|
// The transformed name flows into both the new URI and the rewritten link.
|
|
let renamed = apply_links_space_char("New Page", "-");
|
|
let new_uri = build_new_uri(&PathBuf::from("/wiki"), &renamed, ".wiki").unwrap();
|
|
assert!(new_uri.as_str().ends_with("/wiki/New-Page.wiki"));
|
|
assert_eq!(
|
|
rewrite_wikilink_target("[[Old Name|caption]]", &renamed),
|
|
Some("[[New-Page|caption]]".to_string())
|
|
);
|
|
}
|
|
|
|
// :NuwikiDeleteFile — delete the current page and its on-disk file.
|
|
#[test]
|
|
fn nuwiki_delete_file_emits_delete_workspace_edit() {
|
|
use tower_lsp::lsp_types::{DocumentChangeOperation, DocumentChanges, ResourceOp};
|
|
let uri = Url::parse("file:///wiki/Doomed.wiki").unwrap();
|
|
let mut b = nuwiki_lsp::edits::WorkspaceEditBuilder::new();
|
|
b.file_op(nuwiki_lsp::edits::op_delete(uri.clone()));
|
|
let we = b.build();
|
|
|
|
let DocumentChanges::Operations(ops) = we.document_changes.unwrap() else {
|
|
panic!("expected resource operations");
|
|
};
|
|
assert!(matches!(
|
|
&ops[0],
|
|
DocumentChangeOperation::Op(ResourceOp::Delete(d)) if d.uri == uri
|
|
));
|
|
}
|
|
|
|
// =====================================================================
|
|
// Group 2: Diary
|
|
// =====================================================================
|
|
|
|
// :NuwikiMakeDiaryNote — open today's entry.
|
|
#[test]
|
|
fn nuwiki_make_diary_note_targets_todays_file() {
|
|
let cfg = wiki_cfg("/wiki");
|
|
let today = DiaryDate::from_ymd(2026, 5, 12).unwrap();
|
|
let uri = diary::uri_for_date(&cfg, &today).unwrap();
|
|
assert!(uri.as_str().ends_with("/diary/2026-05-12.wiki"));
|
|
}
|
|
|
|
// :NuwikiMakeYesterdayDiaryNote / :NuwikiMakeTomorrowDiaryNote — step the
|
|
// diary back / forward by one period at the daily cadence.
|
|
#[test]
|
|
fn nuwiki_yesterday_and_tomorrow_target_adjacent_files() {
|
|
let cfg = wiki_cfg("/wiki");
|
|
let yesterday = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
|
let tomorrow = DiaryDate::from_ymd(2026, 5, 13).unwrap();
|
|
assert!(diary::uri_for_date(&cfg, &yesterday)
|
|
.unwrap()
|
|
.as_str()
|
|
.ends_with("/diary/2026-05-11.wiki"));
|
|
assert!(diary::uri_for_date(&cfg, &tomorrow)
|
|
.unwrap()
|
|
.as_str()
|
|
.ends_with("/diary/2026-05-13.wiki"));
|
|
}
|
|
|
|
// :NuwikiDiaryIndex — open the diary index page.
|
|
#[test]
|
|
fn nuwiki_diary_index_resolves_index_uri() {
|
|
let cfg = wiki_cfg("/wiki");
|
|
let uri = diary::index_uri(&cfg).unwrap();
|
|
assert!(uri.as_str().ends_with("/diary/diary.wiki"));
|
|
}
|
|
|
|
// :NuwikiDiaryGenerateLinks — rebuild the diary index from disk entries.
|
|
#[test]
|
|
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
|
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
|
let entries = diary::list_entries(&idx);
|
|
let body = diary::build_index_body(
|
|
&entries,
|
|
"diary",
|
|
"Diary",
|
|
diary::DiarySort::Desc,
|
|
1,
|
|
&[],
|
|
0,
|
|
);
|
|
assert!(body.starts_with("= Diary ="));
|
|
assert!(body.contains("2026-05-12"));
|
|
assert!(body.contains("2026-05-10"));
|
|
assert!(body.contains("2026-04-30"));
|
|
// Newest entry appears before the older one within the same month.
|
|
let p12 = body.find("2026-05-12").unwrap();
|
|
let p10 = body.find("2026-05-10").unwrap();
|
|
assert!(p12 < p10, "expected newest-first ordering");
|
|
}
|
|
|
|
// :NuwikiDiaryNextDay / :NuwikiDiaryPrevDay — walk indexed entries.
|
|
#[test]
|
|
fn nuwiki_diary_next_and_prev_day_walk_entries() {
|
|
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-05-15"]);
|
|
let from = DiaryDate::from_ymd(2026, 5, 12).unwrap();
|
|
assert_eq!(
|
|
diary::next_entry(&idx, &from).unwrap().date.format(),
|
|
"2026-05-15"
|
|
);
|
|
assert_eq!(
|
|
diary::prev_entry(&idx, &from).unwrap().date.format(),
|
|
"2026-05-10"
|
|
);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Group 3: Page generation
|
|
// =====================================================================
|
|
|
|
// :NuwikiTOC — generate / refresh the table of contents.
|
|
#[test]
|
|
fn nuwiki_toc_generates_nested_contents() {
|
|
let src = "= Top =\n== Sub ==\ntext\n";
|
|
let doc = parse(src);
|
|
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
|
let edit =
|
|
ops::toc_edit(src, &doc, &uri, true, "Contents", 1, 0, 0, None).expect("toc edit produced");
|
|
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
|
|
|
let toc = ops::build_toc_text(
|
|
&[
|
|
(1, "Top".into(), "top".into()),
|
|
(2, "Sub".into(), "sub".into()),
|
|
],
|
|
"Contents",
|
|
1,
|
|
0,
|
|
0,
|
|
);
|
|
assert!(toc.contains("= Contents ="));
|
|
assert!(toc.contains("- [[#top|Top]]"));
|
|
assert!(toc.contains(" - [[#sub|Sub]]"));
|
|
}
|
|
|
|
// :NuwikiGenerateLinks — insert a flat list of every page in the wiki.
|
|
#[test]
|
|
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
|
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
|
let text = ops::build_links_text(&pages, "Links", 1, Some("Home"), 0, None);
|
|
assert!(text.contains("= Links ="));
|
|
assert!(text.contains("- [[About]]"));
|
|
assert!(text.contains("- [[Notes]]"));
|
|
assert!(!text.contains("[[Home]]"), "current page excluded");
|
|
|
|
let src = "= Existing =\n";
|
|
let doc = parse(src);
|
|
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
|
assert!(ops::links_edit(
|
|
src,
|
|
&doc,
|
|
&uri,
|
|
"Home",
|
|
&pages,
|
|
true,
|
|
"Generated Links",
|
|
1,
|
|
0,
|
|
None,
|
|
None
|
|
)
|
|
.is_some());
|
|
}
|
|
|
|
// :NuwikiCheckLinks — surface broken links across the workspace.
|
|
#[test]
|
|
fn nuwiki_check_links_distinguishes_broken_from_resolvable() {
|
|
let idx = build_index(
|
|
"/wiki",
|
|
&[("Home", "[[About]] [[Ghost]]\n"), ("About", "= A =\n")],
|
|
);
|
|
let good = LinkTarget {
|
|
kind: LinkKind::Wiki,
|
|
path: Some("About".into()),
|
|
..Default::default()
|
|
};
|
|
let broken = LinkTarget {
|
|
kind: LinkKind::Wiki,
|
|
path: Some("Ghost".into()),
|
|
..Default::default()
|
|
};
|
|
assert!(idx.resolve(&good, "Home").is_some());
|
|
assert!(
|
|
idx.resolve(&broken, "Home").is_none(),
|
|
"broken link unresolved"
|
|
);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Group 4: Tags
|
|
// =====================================================================
|
|
|
|
// :NuwikiSearchTags {tag} — every `:tag:` occurrence.
|
|
#[test]
|
|
fn nuwiki_search_tags_finds_occurrences() {
|
|
let idx = build_index(
|
|
"/wiki",
|
|
&[("Home", ":alpha:beta:\n"), ("Work", ":alpha:\n")],
|
|
);
|
|
let hits = ops::tag_hits(&idx, "alpha");
|
|
assert_eq!(hits.len(), 2);
|
|
assert!(hits.iter().all(|h| h.name == "alpha"));
|
|
assert!(ops::tag_hits(&idx, "nonexistent").is_empty());
|
|
}
|
|
|
|
// :NuwikiGenerateTagLinks [tag] — section linking every page with a tag.
|
|
#[test]
|
|
fn nuwiki_generate_tag_links_builds_section() {
|
|
let idx = build_index(
|
|
"/wiki",
|
|
&[
|
|
("Home", ":alpha:\n"),
|
|
("Work", ":alpha:\n"),
|
|
("Misc", ":beta:\n"),
|
|
],
|
|
);
|
|
let by_tag = ops::tag_pages_snapshot(&idx);
|
|
|
|
let single = ops::build_tag_links_text(&by_tag, Some("alpha"), "Generated Tags", 1, 0).unwrap();
|
|
assert!(single.starts_with("= Tag: alpha ="));
|
|
assert!(single.contains("- [[Home]]"));
|
|
assert!(single.contains("- [[Work]]"));
|
|
|
|
// No-arg variant emits a section per tag.
|
|
let all = ops::build_tag_links_text(&by_tag, None, "Generated Tags", 1, 0).unwrap();
|
|
assert!(all.starts_with("= Generated Tags ="));
|
|
assert!(all.contains("== alpha =="));
|
|
assert!(all.contains("== beta =="));
|
|
|
|
let src = "= Home =\n";
|
|
let doc = parse(src);
|
|
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
|
assert!(ops::tag_links_edit(
|
|
src,
|
|
&doc,
|
|
&uri,
|
|
Some("alpha"),
|
|
&by_tag,
|
|
true,
|
|
"Generated Tags",
|
|
1,
|
|
0,
|
|
None
|
|
)
|
|
.is_some());
|
|
}
|
|
|
|
// :NuwikiRebuildTags — force a full workspace re-index. A rebuild must
|
|
// reflect the current on-disk tags, dropping stale ones.
|
|
#[test]
|
|
fn nuwiki_rebuild_tags_reflects_current_state() {
|
|
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
|
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
|
idx.upsert(uri.clone(), &parse(":old:\n"));
|
|
assert_eq!(idx.tags_for("old").len(), 1);
|
|
|
|
// Re-index after the page changed on disk.
|
|
idx.upsert(uri, &parse(":new:\n"));
|
|
assert!(idx.tags_for("old").is_empty(), "stale tag dropped");
|
|
assert_eq!(idx.tags_for("new").len(), 1);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Group 5: HTML export
|
|
// =====================================================================
|
|
|
|
// :Nuwiki2HTML — render the current page to HTML.
|
|
#[test]
|
|
fn nuwiki_2html_renders_page() {
|
|
let cfg = wiki_cfg("/wiki");
|
|
let doc = parse("%title My Page\n= One =\nbody text\n");
|
|
let html = export::render_page_html(
|
|
&doc,
|
|
Some("<title>{{title}}</title><main>{{content}}</main>".into()),
|
|
"Home",
|
|
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
|
&cfg.html,
|
|
Some(".wiki"),
|
|
HashMap::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(html.contains("<title>My Page</title>"));
|
|
assert!(html.contains("body text"));
|
|
}
|
|
|
|
// :Nuwiki2HTMLBrowse — render then open in the browser. The render output
|
|
// is identical to :Nuwiki2HTML; the browse step is an editor-side open.
|
|
#[test]
|
|
fn nuwiki_2html_browse_shares_render_and_is_advertised() {
|
|
assert!(COMMANDS.contains(&"nuwiki.export.browse"));
|
|
let cfg = wiki_cfg("/wiki");
|
|
let doc = parse("= Page =\nhi\n");
|
|
let html = export::render_page_html(
|
|
&doc,
|
|
Some("{{content}}".into()),
|
|
"Page",
|
|
DiaryDate::today_utc(),
|
|
&cfg.html,
|
|
Some(".wiki"),
|
|
HashMap::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(html.contains("hi"));
|
|
}
|
|
|
|
// :NuwikiAll2HTML[!] — export every page; honour the exclude list and
|
|
// per-page output paths.
|
|
#[test]
|
|
fn nuwiki_all2html_maps_output_paths_and_excludes() {
|
|
let cfg = wiki_cfg("/wiki");
|
|
assert!(export::output_path_for(&cfg.html, "Home").ends_with("Home.html"));
|
|
assert!(
|
|
export::output_path_for(&cfg.html, "diary/2026-05-12").ends_with("diary/2026-05-12.html")
|
|
);
|
|
|
|
let patterns = vec!["_drafts/**".to_string()];
|
|
assert!(export::is_excluded(&patterns, "_drafts/secret"));
|
|
assert!(!export::is_excluded(&patterns, "Home"));
|
|
|
|
// Both incremental and forced (`!`) variants are advertised.
|
|
assert!(COMMANDS.contains(&"nuwiki.export.allToHtml"));
|
|
assert!(COMMANDS.contains(&"nuwiki.export.allToHtmlForce"));
|
|
}
|
|
|
|
// :NuwikiRss — write `rss.xml` summarising recent diary entries.
|
|
#[test]
|
|
fn nuwiki_rss_writes_feed_file() {
|
|
let root = std::env::temp_dir().join(format!("nuwiki-cov-rss-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
let cfg = WikiConfig::from_root(root.clone());
|
|
|
|
let entries = vec![
|
|
diary::DiaryEntry {
|
|
date: DiaryDate::from_ymd(2026, 5, 10).unwrap(),
|
|
uri: Url::from_file_path(root.join("diary/2026-05-10.wiki")).unwrap(),
|
|
},
|
|
diary::DiaryEntry {
|
|
date: DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
|
uri: Url::from_file_path(root.join("diary/2026-05-12.wiki")).unwrap(),
|
|
},
|
|
];
|
|
|
|
let path = export_ops::write_rss(&cfg, &entries).unwrap();
|
|
assert!(path.ends_with("rss.xml"));
|
|
let xml = std::fs::read_to_string(&path).unwrap();
|
|
assert!(xml.contains("<rss version=\"2.0\""));
|
|
// Newest entry listed first.
|
|
let p12 = xml.find("2026-05-12").unwrap();
|
|
let p10 = xml.find("2026-05-10").unwrap();
|
|
assert!(p12 < p10);
|
|
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Group 6: Other
|
|
// =====================================================================
|
|
|
|
// :NuwikiInstall — install/re-install the `nuwiki-ls` binary. This is an
|
|
// editor-side action (binary download / cargo build); it is deliberately
|
|
// NOT an LSP executeCommand, so the server must not advertise it.
|
|
#[test]
|
|
fn nuwiki_install_is_editor_only_not_an_lsp_command() {
|
|
assert!(!COMMANDS.iter().any(|c| c.contains("install")));
|
|
}
|
|
|
|
// =====================================================================
|
|
// Command-surface cross-check: every documented command that maps to an
|
|
// executeCommand handler is actually advertised by the server.
|
|
// =====================================================================
|
|
|
|
#[test]
|
|
fn every_documented_lsp_command_is_advertised() {
|
|
let expected = [
|
|
"nuwiki.wiki.openIndex", // :NuwikiIndex
|
|
"nuwiki.wiki.tabOpenIndex", // :NuwikiTabIndex
|
|
"nuwiki.wiki.listAll", // :NuwikiUISelect
|
|
"nuwiki.wiki.gotoPage", // :NuwikiGoto
|
|
"nuwiki.file.delete", // :NuwikiDeleteFile
|
|
"nuwiki.diary.openToday", // :NuwikiMakeDiaryNote
|
|
"nuwiki.diary.openYesterday", // :NuwikiMakeYesterdayDiaryNote
|
|
"nuwiki.diary.openTomorrow", // :NuwikiMakeTomorrowDiaryNote
|
|
"nuwiki.diary.openIndex", // :NuwikiDiaryIndex
|
|
"nuwiki.diary.generateIndex", // :NuwikiDiaryGenerateLinks
|
|
"nuwiki.diary.next", // :NuwikiDiaryNextDay
|
|
"nuwiki.diary.prev", // :NuwikiDiaryPrevDay
|
|
"nuwiki.toc.generate", // :NuwikiTOC
|
|
"nuwiki.links.generate", // :NuwikiGenerateLinks
|
|
"nuwiki.workspace.checkLinks", // :NuwikiCheckLinks
|
|
"nuwiki.tags.search", // :NuwikiSearchTags
|
|
"nuwiki.tags.generateLinks", // :NuwikiGenerateTagLinks
|
|
"nuwiki.tags.rebuild", // :NuwikiRebuildTags
|
|
"nuwiki.export.currentToHtml", // :Nuwiki2HTML
|
|
"nuwiki.export.browse", // :Nuwiki2HTMLBrowse
|
|
"nuwiki.export.allToHtml", // :NuwikiAll2HTML
|
|
"nuwiki.export.rss", // :NuwikiRss
|
|
];
|
|
for cmd in expected {
|
|
assert!(COMMANDS.contains(&cmd), "server must advertise {cmd}");
|
|
}
|
|
}
|