use nautilus_schema::{
analyze, completion, goto_definition, hover, semantic_tokens, CompletionKind, SemanticKind,
Severity,
};
const VALID: &str = r#"
datasource db {
provider = "postgresql"
url = "postgresql://localhost/test"
}
model User {
id Int @id
email String @unique
name String
role Role
posts Post[]
}
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id])
}
enum Role {
Admin
Member
}
"#;
#[test]
fn analyze_valid_returns_no_diagnostics() {
let r = analyze(VALID);
assert!(
r.diagnostics.is_empty(),
"expected no diagnostics, got: {:#?}",
r.diagnostics
);
assert!(r.ast.is_some(), "ast should be Some");
assert!(r.ir.is_some(), "ir should be Some");
}
#[test]
fn analyze_exposes_ir_models() {
let r = analyze(VALID);
let ir = r.ir.unwrap();
assert!(ir.models.contains_key("User"));
assert!(ir.models.contains_key("Post"));
assert!(ir.enums.contains_key("Role"));
}
#[test]
fn analyze_lex_error_populates_diagnostics() {
let src = "model User { id # Int @id }";
let r = analyze(src);
assert!(!r.diagnostics.is_empty());
assert_eq!(r.diagnostics[0].severity, Severity::Error);
}
#[test]
fn analyze_parse_recovery_error_collected() {
let src = r#"
!!!garbage!!!
model Good {
id Int @id
}
"#;
let r = analyze(src);
assert!(r.ast.is_some());
assert!(!r.diagnostics.is_empty(), "expected parse diagnostics");
}
#[test]
fn analyze_validation_error_has_span() {
let src = r#"
model Dupe {
id Int @id
}
model Dupe {
id Int @id
}
"#;
let r = analyze(src);
let with_span: Vec<_> = r
.diagnostics
.iter()
.filter(|d| d.span.start > 0 || d.span.end > 0)
.collect();
assert!(
!with_span.is_empty(),
"expected at least one diagnostic with a real span"
);
}
#[test]
fn analyze_collects_multiple_datasource_diagnostics() {
let src = r#"
datasource db {
provider = 123
url = env("DATABASE_URL", 1)
foo = "bar"
}
"#;
let r = analyze(src);
let messages: Vec<&str> = r.diagnostics.iter().map(|d| d.message.as_str()).collect();
assert!(
r.ir.is_none(),
"IR should be absent on config validation errors"
);
assert!(
messages
.iter()
.any(|msg| msg.contains("Unknown field 'foo' in datasource block")),
"missing unknown-field diagnostic: {:?}",
messages
);
assert!(
messages
.iter()
.any(|msg| msg.contains("Datasource 'provider' must be a string literal")),
"missing provider diagnostic: {:?}",
messages
);
assert!(
messages.iter().any(
|msg| msg.contains("Datasource 'url' env() call requires a single string argument")
),
"missing url diagnostic: {:?}",
messages
);
}
#[test]
fn analyze_collects_multiple_generator_diagnostics_and_later_warnings() {
let src = r#"
generator client {
provider = 123
output = 456
interface = 789
foo = "bar"
}
model User {
id Int @id
updatedAt DateTime @updatedAt @default(now())
}
"#;
let r = analyze(src);
let messages: Vec<&str> = r.diagnostics.iter().map(|d| d.message.as_str()).collect();
assert!(
r.ir.is_none(),
"IR should be absent on config validation errors"
);
assert!(
messages
.iter()
.any(|msg| msg.contains("Generator 'provider' must be a string literal")),
"missing provider diagnostic: {:?}",
messages
);
assert!(
messages
.iter()
.any(|msg| msg.contains("Generator 'output' must be a string literal")),
"missing output diagnostic: {:?}",
messages
);
assert!(
messages
.iter()
.any(|msg| msg.contains("Generator 'interface' must be a string literal")),
"missing interface diagnostic: {:?}",
messages
);
assert!(
messages
.iter()
.any(|msg| msg.contains("Unknown field 'foo' in generator block")),
"missing unknown-field diagnostic: {:?}",
messages
);
assert!(
r.diagnostics.iter().any(|d| {
d.severity == Severity::Warning && d.message.contains("both @updatedAt and @default")
}),
"missing later warning: {:?}",
r.diagnostics
);
}
#[test]
fn completion_at_empty_source_returns_top_level_keywords() {
let items = completion("", 0);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"model"), "missing 'model': {:?}", labels);
assert!(labels.contains(&"enum"), "missing 'enum': {:?}", labels);
assert!(
labels.contains(&"datasource"),
"missing 'datasource': {:?}",
labels
);
assert!(
labels.contains(&"generator"),
"missing 'generator': {:?}",
labels
);
assert!(
items.iter().all(|i| i.kind == CompletionKind::Keyword),
"all top-level items should be Keyword kind"
);
}
#[test]
fn completion_inside_datasource_only_contains_datasource_fields() {
let src = "datasource db {\n \n}";
let offset = src.find("\n \n").unwrap() + 3;
let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"provider"),
"missing provider: {:?}",
labels
);
assert!(labels.contains(&"url"), "missing url: {:?}", labels);
assert!(
!labels.contains(&"output"),
"unexpected output: {:?}",
labels
);
assert!(
!labels.contains(&"interface"),
"unexpected interface: {:?}",
labels
);
assert!(
!labels.contains(&"recursive_type_depth"),
"unexpected recursive_type_depth: {:?}",
labels
);
}
#[test]
fn completion_inside_generator_only_contains_generator_fields() {
let src = "generator client {\n \n}";
let offset = src.find("\n \n").unwrap() + 3;
let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"provider"),
"missing provider: {:?}",
labels
);
assert!(labels.contains(&"output"), "missing output: {:?}", labels);
assert!(
labels.contains(&"interface"),
"missing interface: {:?}",
labels
);
assert!(
labels.contains(&"recursive_type_depth"),
"missing recursive_type_depth: {:?}",
labels
);
assert!(!labels.contains(&"url"), "unexpected url: {:?}", labels);
}
#[test]
fn completion_inside_default_args_excludes_unsupported_functions() {
let src = "model User {\n id Uuid @default()\n}";
let offset = src.find("@default(").unwrap() + "@default(".len();
let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"autoincrement()"), "missing autoincrement");
assert!(labels.contains(&"now()"), "missing now");
assert!(labels.contains(&"uuid()"), "missing uuid");
assert!(!labels.contains(&"cuid()"), "unexpected cuid: {:?}", labels);
assert!(
!labels.contains(&"dbgenerated(\"expr\")"),
"unexpected dbgenerated: {:?}",
labels
);
}
#[test]
fn completion_inside_model_contains_scalar_types() {
let src = "model User {\n \n}";
let offset = src.find("\n \n").unwrap() + 3;
let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
for ty in &["String", "Int", "Boolean", "Float", "DateTime", "Uuid"] {
assert!(labels.contains(ty), "missing type '{}': {:?}", ty, labels);
}
}
#[test]
fn completion_inside_model_includes_user_defined_types() {
let src =
"type Address {\n street String\n}\n\nmodel Post {\n author \n}\n\nmodel User {\n id Int @id\n}\n";
let offset = src.find("author ").unwrap() + "author ".len();
let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"User"),
"User model should appear as a type completion: {:?}",
labels
);
assert!(
labels.contains(&"Address"),
"Address composite type should appear as a type completion: {:?}",
labels
);
}
#[test]
fn completion_inside_type_only_includes_scalar_and_enum_types() {
let src = r#"
type Address {
label
}
enum LabelKind {
Home
}
model User {
id Int @id
}
"#;
let offset = src.find("label ").unwrap() + "label ".len();
let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"String"));
assert!(labels.contains(&"LabelKind"));
assert!(
!labels.contains(&"User"),
"model refs are invalid in type blocks"
);
}
#[test]
fn completion_after_at_returns_field_attributes() {
let src = "model User {\n id Int @\n}";
let offset = src.find('@').unwrap() + 1;
let items = completion(src, offset);
assert!(
items
.iter()
.any(|i| i.kind == CompletionKind::FieldAttribute),
"expected field attribute completions after '@': {:?}",
items
);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"id"),
"missing 'id' attribute: {:?}",
labels
);
assert!(
labels.contains(&"unique"),
"missing 'unique' attribute: {:?}",
labels
);
}
#[test]
fn completion_after_atat_returns_model_attributes() {
let src = "model User {\n id Int @id\n @@\n}";
let offset = src.find("@@").unwrap() + 2;
let items = completion(src, offset);
assert!(
items
.iter()
.any(|i| i.kind == CompletionKind::ModelAttribute),
"expected model attribute completions after '@@': {:?}",
items
);
}
#[test]
fn hover_on_model_field_returns_type_info() {
let offset = VALID.find("email").unwrap() + 2;
let h = hover(VALID, offset);
assert!(h.is_some(), "hover returned None for field 'email'");
let info = h.unwrap();
assert!(
info.content.contains("email") || info.content.contains("String"),
"unexpected hover content: {}",
info.content
);
assert!(info.span.is_some());
}
#[test]
fn hover_on_provider_field_mentions_real_generator_providers() {
let src = r#"
generator client {
provider = "nautilus-client-rs"
}
"#;
let offset = src.find("provider").unwrap() + 2;
let h = hover(src, offset).expect("hover returned None");
assert!(
h.content.contains("nautilus-client-rs")
&& h.content.contains("nautilus-client-py")
&& h.content.contains("nautilus-client-js"),
"unexpected hover content: {}",
h.content
);
assert!(
!h.content.contains("\"python\""),
"unexpected stale provider alias in hover: {}",
h.content
);
}
#[test]
fn hover_on_default_attribute_omits_unsupported_functions() {
let src = r#"
model User {
id Uuid @default(uuid())
}
"#;
let offset = src.find("@default").unwrap() + 2;
let h = hover(src, offset).expect("hover returned None");
assert!(h.content.contains("autoincrement()"));
assert!(h.content.contains("now()"));
assert!(h.content.contains("uuid()"));
assert!(
!h.content.contains("cuid()"),
"unexpected hover: {}",
h.content
);
assert!(
!h.content.contains("dbgenerated"),
"unexpected hover: {}",
h.content
);
}
#[test]
fn hover_on_model_declaration_returns_model_summary() {
let offset = VALID.find("model User").unwrap() + 2;
let h = hover(VALID, offset);
assert!(h.is_some(), "hover returned None on model declaration");
let info = h.unwrap();
assert!(
info.content.contains("User"),
"hover content should mention model name: {}",
info.content
);
}
#[test]
fn hover_on_enum_returns_variant_list() {
let offset = VALID.find("enum Role").unwrap() + 2;
let h = hover(VALID, offset);
assert!(h.is_some(), "hover returned None on enum");
let info = h.unwrap();
assert!(info.content.contains("Admin") || info.content.contains("Member"));
}
#[test]
fn hover_outside_all_declarations_returns_none() {
let src = "// comment\nmodel User { id Int @id }\n";
let h = hover(src, 2);
let _ = h;
}
#[test]
fn goto_definition_user_type_resolves_to_model_span() {
let offset = VALID.find("author User").unwrap() + "author ".len() + 1;
let span = goto_definition(VALID, offset);
assert!(span.is_some(), "goto_definition returned None");
let s = span.unwrap();
let slice = &VALID[s.start..s.end];
assert!(
slice.contains("User"),
"span does not cover User declaration: {:?}",
slice
);
}
#[test]
fn goto_definition_scalar_field_returns_none() {
let offset = VALID.find("email String").unwrap() + "email ".len() + 1;
let span = goto_definition(VALID, offset);
assert!(
span.is_none(),
"expected None for scalar type, got: {:?}",
span
);
}
#[test]
fn goto_definition_enum_field_resolves_to_enum_span() {
let offset = VALID.find("role Role").unwrap() + "role ".len() + 1;
let span = goto_definition(VALID, offset);
assert!(
span.is_some(),
"goto_definition returned None for enum field"
);
let s = span.unwrap();
let slice = &VALID[s.start..s.end];
assert!(
slice.contains("Role"),
"span does not cover Role declaration: {:?}",
slice
);
}
#[test]
fn goto_definition_composite_type_field_resolves_to_type_span() {
let src = r#"
type Address {
street String
}
model User {
id Int @id
address Address
}
"#;
let offset = src.find("address Address").unwrap() + "address ".len() + 1;
let span = goto_definition(src, offset).expect("goto_definition returned None");
let slice = &src[span.start..span.end];
assert!(
slice.contains("type Address"),
"span does not cover Address type declaration: {:?}",
slice
);
}
#[test]
fn goto_definition_type_field_enum_resolves_to_enum_span() {
let src = r#"
type Address {
kind Role
}
enum Role {
Home
}
"#;
let offset = src.find("kind Role").unwrap() + "kind ".len() + 1;
let span = goto_definition(src, offset).expect("goto_definition returned None");
let slice = &src[span.start..span.end];
assert!(
slice.contains("enum Role"),
"span does not cover Role enum declaration: {:?}",
slice
);
}
#[test]
fn semantic_tokens_include_type_block_references() {
let src = r#"
type Address {
kind Role
}
enum Role {
Home
}
"#;
let analysis = analyze(src);
let ast = analysis.ast.as_ref().expect("ast");
let tokens = semantic_tokens(ast, &analysis.tokens);
assert!(
tokens
.iter()
.any(|token| token.kind == SemanticKind::EnumRef
&& &src[token.span.start..token.span.end] == "Role"),
"expected enum semantic token for Role reference inside type block"
);
}