use std::collections::BTreeSet;
use serde::Deserialize;
const ENUM_VARIANTS: &[&str] = &[
"Callout", "Code", "Data", "Decision", "Details", "Diagram", "Figure",
"Metric", "Quote", "Summary", "Tasks",
"Columns", "Divider", "Section", "Tabs",
"BeforeAfter", "Comparison", "Cta", "Embed", "Faq", "Features",
"Footer", "Form", "Gallery", "Gate", "Hero", "HeroImage", "Logo",
"Nav", "Pipeline", "PostGrid", "PricingTable", "ProductCard", "Site", "Page",
"Stats", "Steps", "Style", "Testimonial", "Toc",
"Action", "Board", "ChatInput", "Dashboard", "Feed", "FilterBar",
"List", "Search",
"Chart", "Editor", "SplitPane",
"App", "Auth", "Binding", "Build", "Cicd", "Concurrency",
"Crates", "Deploy", "DeployUrls", "Domains", "Health",
"InfraDatabase", "InfraEnv", "Model", "Route", "Schema",
"Smoke", "Use", "Volumes",
"AppDeploy", "AppEnv",
"Badge", "InfoCard", "Row",
"AppShell", "BlockEditor", "ChatInputSimple", "ChatThread",
"CodeEditor", "CommandPalette", "Drawer", "LogStream", "Modal",
"NavTree", "Panel", "ProblemList", "Progress", "Sidebar",
"SuggestionChips", "TabBar", "TabContent", "Terminal", "Toolbar",
];
#[derive(Debug, Deserialize)]
struct Registry {
meta: RegistryMeta,
blocks: std::collections::BTreeMap<String, BlockEntry>,
}
#[derive(Debug, Deserialize)]
struct RegistryMeta {
spec_version: String,
total_blocks: usize,
#[allow(dead_code)]
registry_updated: String,
}
#[derive(Debug, Deserialize)]
struct BlockEntry {
status: String,
enum_variant: String,
#[allow(dead_code)]
purpose: String,
#[allow(dead_code)]
category: String,
#[allow(dead_code)]
attributes: Vec<String>,
#[allow(dead_code)]
degradation: String,
}
#[test]
fn spec_compliance() {
let registry: Registry = toml::from_str(
include_str!("../spec/blocks.toml")
).expect("spec/blocks.toml must parse as valid TOML");
let enum_set: BTreeSet<&str> = ENUM_VARIANTS.iter().copied().collect();
let registry_implemented: BTreeSet<&str> = registry.blocks.values()
.filter(|b| b.status == "implemented")
.map(|b| b.enum_variant.as_str())
.collect();
let registry_all: BTreeSet<&str> = registry.blocks.values()
.map(|b| b.enum_variant.as_str())
.collect();
let code_without_spec: Vec<&&str> = enum_set.difference(®istry_all).collect();
assert!(
code_without_spec.is_empty(),
"\n\nSPEC VIOLATION: Block variants exist in code but are NOT defined in the spec:\n \
{:?}\n\n\
The spec is the authority. Either:\n \
1. Add these blocks to spec/blocks.toml (if the spec should define them)\n \
2. Remove these variants from types.rs (if they shouldn't exist)\n",
code_without_spec
);
let registry_without_code: Vec<&&str> = registry_implemented.difference(&enum_set).collect();
assert!(
registry_without_code.is_empty(),
"\n\nREGISTRY LIE: blocks.toml says these are implemented, but no Block variant exists:\n \
{:?}\n\n\
Either implement the variant in types.rs or change status to \"planned\" in blocks.toml.\n",
registry_without_code
);
let planned: Vec<(&String, &str)> = registry.blocks.iter()
.filter(|(_, b)| b.status == "planned")
.map(|(name, b)| (name, b.enum_variant.as_str()))
.collect();
if !planned.is_empty() {
eprintln!("\n--- Spec backlog: {} blocks defined but not yet implemented ---", planned.len());
for (name, variant) in &planned {
eprintln!(" ::{} (Block::{})", name, variant);
}
eprintln!("---\n");
}
assert_eq!(
registry.meta.total_blocks,
registry.blocks.len(),
"meta.total_blocks ({}) doesn't match actual block count ({})",
registry.meta.total_blocks,
registry.blocks.len()
);
}
#[test]
fn lint_rule_registry_matches_implementation() {
let registry = surf_parse::lint::rule_registry();
let implemented: BTreeSet<&str> = surf_parse::lint::all_rules()
.iter()
.map(|r| r.id())
.collect();
let style_ids: BTreeSet<&str> = registry
.iter()
.filter(|(_, m)| m.layer == "style")
.map(|(id, _)| id.as_str())
.collect();
assert_eq!(
implemented, style_ids,
"\n\nRULE DRIFT: style rules in spec/rules.toml and src/lint.rs::all_rules() \
must match exactly.\n registered: {style_ids:?}\n implemented: {implemented:?}\n"
);
let syntax_ids: BTreeSet<&str> = registry
.iter()
.filter(|(_, m)| m.layer == "syntax")
.map(|(id, _)| id.as_str())
.collect();
let parse_ids: BTreeSet<&str> = surf_parse::lint::PARSE_RULE_IDS.iter().copied().collect();
assert_eq!(
syntax_ids, parse_ids,
"syntax rules in spec/rules.toml must match lint::PARSE_RULE_IDS"
);
for (id, meta) in registry {
let expected_prefix = match meta.layer.as_str() {
"syntax" => 'P',
"style" => 'L',
other => panic!(
"rule {id}: unknown layer {other:?} (V-codes live in validate.rs, not the registry)"
),
};
assert!(
id.starts_with(expected_prefix),
"rule {id}: layer {:?} requires a {expected_prefix}-prefixed id",
meta.layer
);
assert_eq!(
meta.fix_safety.is_some(),
meta.fixable,
"rule {id}: fix_safety must be set exactly when fixable"
);
if let Some(safety) = &meta.fix_safety {
assert!(
safety == "safe" || safety == "suggested",
"rule {id}: fix_safety must be \"safe\" or \"suggested\", got {safety:?}"
);
}
}
assert_eq!(
surf_parse::lint::rule_registry_total(),
registry.len(),
"meta.total_rules doesn't match actual rule count"
);
}
#[test]
fn lint_known_block_names_come_from_blocks_toml() {
let registry: Registry = toml::from_str(include_str!("../spec/blocks.toml"))
.expect("spec/blocks.toml must parse as valid TOML");
let spec_names: BTreeSet<&str> = registry.blocks.keys().map(String::as_str).collect();
let known: BTreeSet<&str> = surf_parse::lint::known_block_names()
.iter()
.map(String::as_str)
.collect();
assert_eq!(
known, spec_names,
"lint::known_block_names() must be exactly the spec/blocks.toml names"
);
for extra in surf_parse::lint::EXTRA_KNOWN_BLOCK_NAMES {
assert!(
!spec_names.contains(extra),
"'{extra}' is now registered in spec/blocks.toml — remove it from \
lint::EXTRA_KNOWN_BLOCK_NAMES"
);
}
}
#[test]
fn app_spec_v1_doc_is_valid_surfdoc() {
let source = include_str!("../spec/app-spec-v1.surf");
let result = surf_parse::parse(source);
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.severity == surf_parse::Severity::Error)
.collect();
assert!(errors.is_empty(), "spec doc must parse without errors: {errors:?}");
let fm = result
.doc
.front_matter
.as_ref()
.expect("spec doc must have front matter");
assert_eq!(fm.title.as_deref(), Some("SurfDoc App Spec v1"));
assert_eq!(fm.doc_type, Some(surf_parse::DocType::Doc));
assert_eq!(fm.version, Some(1));
assert_eq!(fm.created.as_deref(), Some("2026-07-15"));
assert_eq!(fm.status, Some(surf_parse::DocStatus::Active));
let summary_idx = result
.doc
.blocks
.iter()
.position(|b| matches!(b, surf_parse::Block::Summary { .. }))
.expect("spec doc must contain a ::summary block");
assert!(summary_idx <= 1, "::summary must be at the top of the document");
assert!(
result
.doc
.blocks
.iter()
.any(|b| matches!(b, surf_parse::Block::Callout { .. })),
"spec doc must contain a ::callout"
);
}