use harn_hostlib::{
ast::AstCapability, code_index::CodeIndexCapability, fs_watch::FsWatchCapability,
scanner::ScannerCapability, schemas, tools::ToolsCapability, BuiltinRegistry,
HostlibCapability, HostlibError, HostlibRegistry,
};
fn collect_into_registry<C: HostlibCapability>(cap: C) -> BuiltinRegistry {
let mut registry = BuiltinRegistry::new();
cap.register_builtins(&mut registry);
registry
}
#[test]
fn ast_capability_registers_documented_methods() {
let registry = collect_into_registry(AstCapability);
let names: Vec<_> = registry.iter().map(|b| b.name).collect();
assert_eq!(
names,
vec![
"hostlib_ast_parse_file",
"hostlib_ast_symbols",
"hostlib_ast_outline",
"hostlib_ast_parse_errors",
"hostlib_ast_undefined_names",
"hostlib_ast_function_body",
"hostlib_ast_function_bodies",
"hostlib_ast_extract_imports",
"hostlib_ast_symbol_extract",
"hostlib_ast_symbol_delete",
"hostlib_ast_symbol_replace",
"hostlib_ast_bracket_balance",
]
);
let expected_missing: &[(&str, &str)] = &[
("hostlib_ast_parse_file", "path"),
("hostlib_ast_symbols", "path"),
("hostlib_ast_outline", "path"),
("hostlib_ast_parse_errors", "content_or_path"),
("hostlib_ast_undefined_names", "content_or_path"),
("hostlib_ast_function_body", "function_name"),
("hostlib_ast_function_bodies", "names"),
("hostlib_ast_extract_imports", "source"),
("hostlib_ast_symbol_extract", "source"),
("hostlib_ast_symbol_delete", "source"),
("hostlib_ast_symbol_replace", "source"),
("hostlib_ast_bracket_balance", "source"),
];
for (name, expected_param) in expected_missing {
let entry = registry.find(name).expect("registered");
let err = (entry.handler)(&[]).expect_err("must reject empty args");
match err {
HostlibError::MissingParameter { builtin, param } => {
assert_eq!(builtin, *name);
assert_eq!(param, *expected_param);
}
other => panic!("expected MissingParameter for {name}, got {other:?}"),
}
}
}
#[test]
fn code_index_capability_registers_documented_methods() {
let registry = collect_into_registry(CodeIndexCapability::new());
let names: Vec<_> = registry.iter().map(|b| b.name).collect();
assert_eq!(
names,
vec![
"hostlib_code_index_query",
"hostlib_code_index_rebuild",
"hostlib_code_index_stats",
"hostlib_code_index_imports_for",
"hostlib_code_index_importers_of",
"hostlib_code_index_path_to_id",
"hostlib_code_index_id_to_path",
"hostlib_code_index_file_ids",
"hostlib_code_index_file_meta",
"hostlib_code_index_file_hash",
"hostlib_code_index_read_range",
"hostlib_code_index_reindex_file",
"hostlib_code_index_trigram_query",
"hostlib_code_index_extract_trigrams",
"hostlib_code_index_word_get",
"hostlib_code_index_deps_get",
"hostlib_code_index_outline_get",
"hostlib_code_index_current_seq",
"hostlib_code_index_changes_since",
"hostlib_code_index_version_record",
"hostlib_code_index_agent_register",
"hostlib_code_index_agent_heartbeat",
"hostlib_code_index_agent_unregister",
"hostlib_code_index_lock_try",
"hostlib_code_index_lock_release",
"hostlib_code_index_status",
"hostlib_code_index_current_agent_id",
]
);
let stats = registry
.find("hostlib_code_index_stats")
.expect("registered");
let value = (stats.handler)(&[]).expect("stats works on an empty index");
match value {
harn_vm::VmValue::Dict(_) => {}
other => panic!("expected dict response from stats, got {other:?}"),
}
}
#[test]
fn scanner_capability_registers_documented_methods() {
let registry = collect_into_registry(ScannerCapability);
let names: Vec<_> = registry.iter().map(|b| b.name).collect();
assert_eq!(
names,
vec![
"hostlib_scanner_scan_project",
"hostlib_scanner_scan_incremental"
]
);
for name in &[
"hostlib_scanner_scan_project",
"hostlib_scanner_scan_incremental",
] {
let entry = registry.find(name).expect("registered");
let err = (entry.handler)(&[]).expect_err("must reject empty args");
assert!(
!matches!(err, HostlibError::Unimplemented { .. }),
"scanner method {name} should be implemented, got {err:?}"
);
}
}
#[test]
fn fs_watch_capability_registers_documented_methods() {
let registry = collect_into_registry(FsWatchCapability);
let names: Vec<_> = registry.iter().map(|b| b.name).collect();
assert_eq!(
names,
vec!["hostlib_fs_watch_subscribe", "hostlib_fs_watch_unsubscribe"]
);
for entry in registry.iter() {
let err = (entry.handler)(&[]).expect_err("handler must reject empty args");
assert!(
!matches!(err, HostlibError::Unimplemented { .. }),
"fs_watch method {} should be implemented, got {err:?}",
entry.name
);
}
}
#[test]
fn tools_capability_registers_documented_methods() {
let registry = collect_into_registry(ToolsCapability);
let names: Vec<_> = registry.iter().map(|b| b.name).collect();
assert_eq!(
names,
vec![
"hostlib_tools_search",
"hostlib_tools_read_file",
"hostlib_tools_write_file",
"hostlib_tools_delete_file",
"hostlib_tools_list_directory",
"hostlib_tools_get_file_outline",
"hostlib_tools_git",
"hostlib_tools_run_command",
"hostlib_tools_read_command_output",
"hostlib_tools_run_test",
"hostlib_tools_run_build_command",
"hostlib_tools_inspect_test_results",
"hostlib_tools_manage_packages",
"hostlib_tools_cancel_handle",
"hostlib_enable",
]
);
harn_hostlib::tools::permissions::reset();
let gated_methods = [
"hostlib_tools_search",
"hostlib_tools_read_file",
"hostlib_tools_write_file",
"hostlib_tools_delete_file",
"hostlib_tools_list_directory",
"hostlib_tools_get_file_outline",
"hostlib_tools_git",
"hostlib_tools_run_command",
"hostlib_tools_read_command_output",
"hostlib_tools_run_test",
"hostlib_tools_run_build_command",
"hostlib_tools_inspect_test_results",
"hostlib_tools_manage_packages",
"hostlib_tools_cancel_handle",
];
for name in gated_methods {
let entry = registry.find(name).expect("registered");
let err = (entry.handler)(&[]).expect_err("disabled by default");
match err {
HostlibError::Backend { builtin, message } => {
assert_eq!(builtin, name);
assert!(
message.contains("hostlib_enable"),
"gating error must point users at hostlib_enable: {message}"
);
}
other => panic!("expected Backend gate error for {name}, got {other:?}"),
}
}
}
#[test]
fn install_default_wires_every_module_into_a_vm() {
let mut vm = harn_vm::Vm::new();
let registry = harn_hostlib::install_default(&mut vm);
assert_eq!(
registry.modules(),
&["ast", "code_index", "scanner", "fs_watch", "tools"]
);
assert!(registry.builtins().len() >= 57);
}
#[test]
fn every_registered_builtin_has_request_and_response_schemas() {
let registry = HostlibRegistry::new()
.with(AstCapability)
.with(CodeIndexCapability::new())
.with(ScannerCapability)
.with(FsWatchCapability)
.with(ToolsCapability);
for entry in registry.builtins().iter() {
assert!(
schemas::lookup(entry.module, entry.method, schemas::SchemaKind::Request).is_some(),
"missing request schema for {}.{}",
entry.module,
entry.method
);
assert!(
schemas::lookup(entry.module, entry.method, schemas::SchemaKind::Response).is_some(),
"missing response schema for {}.{}",
entry.module,
entry.method
);
}
}
#[test]
fn every_schema_parses_as_valid_json_schema_2020_12() {
for (module, method, kind, body) in schemas::SCHEMAS {
let value: serde_json::Value = serde_json::from_str(body).unwrap_or_else(|err| {
panic!("schema for {module}.{method} ({kind:?}) is not valid JSON: {err}")
});
let dialect = value
.get("$schema")
.and_then(|v| v.as_str())
.expect("every shipped schema must declare its dialect via $schema");
assert!(
dialect.contains("draft/2020-12"),
"schema for {module}.{method} ({kind:?}) declares unexpected dialect: {dialect}"
);
assert!(
value.is_object(),
"schema for {module}.{method} ({kind:?}) must be a JSON object"
);
let object = value.as_object().unwrap();
assert!(
object.contains_key("type") || object.contains_key("$ref"),
"schema for {module}.{method} ({kind:?}) must declare `type` or `$ref`"
);
}
}