use super::*;
use crate::snippets::session::SessionSpec;
use crate::snippets::types::{SnippetMetadata, SourceOrigin};
use crate::snippets::validators::SnippetValidator;
struct UnreachableValidator;
impl SnippetValidator for UnreachableValidator {
fn language(&self) -> crate::snippets::types::Language {
crate::snippets::types::Language::TypeScript
}
fn is_available(&self) -> bool {
true
}
fn validate(
&self,
_snippet: &Snippet,
_level: ValidationLevel,
_timeout_secs: u64,
) -> Result<(SnippetStatus, Option<String>)> {
panic!("this validator must never be invoked: session preparation should short-circuit first");
}
fn max_level(&self) -> ValidationLevel {
ValidationLevel::Run
}
}
struct MissingToolchainValidator;
impl SnippetValidator for MissingToolchainValidator {
fn language(&self) -> crate::snippets::types::Language {
crate::snippets::types::Language::TypeScript
}
fn is_available(&self) -> bool {
false
}
fn validate(
&self,
_snippet: &Snippet,
_level: ValidationLevel,
_timeout_secs: u64,
) -> Result<(SnippetStatus, Option<String>)> {
panic!("an unavailable toolchain must never be invoked");
}
fn max_level(&self) -> ValidationLevel {
ValidationLevel::Run
}
}
struct GenuinelyBrokenValidator;
impl SnippetValidator for GenuinelyBrokenValidator {
fn language(&self) -> crate::snippets::types::Language {
crate::snippets::types::Language::TypeScript
}
fn is_available(&self) -> bool {
true
}
fn validate(
&self,
_snippet: &Snippet,
_level: ValidationLevel,
_timeout_secs: u64,
) -> Result<(SnippetStatus, Option<String>)> {
Ok((SnippetStatus::Fail, Some("expected `;`, found end of file".to_string())))
}
fn max_level(&self) -> ValidationLevel {
ValidationLevel::Run
}
}
fn typescript_snippet() -> Snippet {
Snippet {
id: None,
path: "example.md".into(),
language: crate::snippets::types::Language::TypeScript,
title: None,
code: "const value: number = 1;".into(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: "example.md".into(),
line: 1,
block_index: 0,
},
}
}
fn timing_out_session(working_directory: &std::path::Path) -> HashMap<String, SessionSpec> {
HashMap::from([(
"typescript".to_string(),
SessionSpec {
language: crate::snippets::types::Language::TypeScript,
working_directory: working_directory.to_path_buf(),
manifest: None,
before: vec![sleep_hook(2)],
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
},
)])
}
#[cfg(unix)]
fn sleep_hook(seconds: u64) -> String {
format!("sleep {seconds}")
}
#[cfg(windows)]
fn sleep_hook(seconds: u64) -> String {
format!("ping -n {} 127.0.0.1", seconds + 1)
}
#[test]
fn session_preparation_timeout_is_an_ordering_problem_not_a_bare_error_on_the_parallel_path() {
let directory = tempfile::tempdir().expect("session directory");
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(UnreachableValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
timeout_secs: 1,
sessions: timing_out_session(directory.path()),
fail_fast: false,
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.total, 1);
assert_eq!(summary.errors, 0, "an unbuilt artifact must not count as a bare error");
assert_eq!(summary.failed, 0, "an unbuilt artifact is not a snippet failure");
assert_eq!(summary.unavailable, 1);
assert_eq!(summary.unresolved_dependency, 1);
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Unavailable);
assert!(outcome.unresolved_dependency);
let message = outcome.message.as_deref().unwrap_or_default();
assert!(
message.contains("ordering problem"),
"message must name the ordering problem, not read as a bare timeout: {message}"
);
}
#[test]
fn session_preparation_timeout_is_an_ordering_problem_not_a_bare_error_on_the_fail_fast_path() {
let directory = tempfile::tempdir().expect("session directory");
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(UnreachableValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
timeout_secs: 1,
sessions: timing_out_session(directory.path()),
fail_fast: true,
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.total, 1);
assert_eq!(summary.errors, 0, "an unbuilt artifact must not count as a bare error");
assert_eq!(summary.unavailable, 1);
assert_eq!(summary.unresolved_dependency, 1);
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Unavailable);
assert!(outcome.unresolved_dependency);
assert!(
outcome
.message
.as_deref()
.unwrap_or_default()
.contains("ordering problem")
);
}
#[test]
fn missing_toolchain_is_unavailable_but_not_flagged_as_unresolved_dependency() {
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(MissingToolchainValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.unavailable, 1);
assert_eq!(
summary.unresolved_dependency, 0,
"a missing toolchain is not the same problem as an unbuilt artifact"
);
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Unavailable);
assert!(!outcome.unresolved_dependency);
}
#[test]
fn a_genuinely_broken_snippet_stays_a_real_failure() {
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(GenuinelyBrokenValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.failed, 1);
assert_eq!(summary.unresolved_dependency, 0);
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Fail);
assert!(!outcome.unresolved_dependency);
}
#[test]
fn finalize_result_keeps_a_real_type_error_as_fail_with_the_compiler_message() {
let validator = crate::snippets::validators::typescript::TypeScriptValidator;
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let diagnostic = "snippet.ts(1,7): error TS2322: Type 'number' is not assignable to type 'string'.";
let outcome = ValidationOutcome {
status: SnippetStatus::Fail,
message: Some(diagnostic.to_string()),
duration_ms: 5,
};
let result = finalize_result(
&typescript_snippet(),
&validator,
&config,
None,
ValidationLevel::Compile,
outcome,
);
assert_eq!(result.status, SnippetStatus::Fail, "got: {result:?}");
assert!(
!result.unresolved_dependency,
"a real type error must not be flagged as a dependency gap"
);
assert_eq!(
result.message.as_deref(),
Some(diagnostic),
"a real type error's message must stay the compiler's own text verbatim, not be recaptioned \
as a missing dependency"
);
assert!(
!result.message.as_deref().unwrap_or_default().contains("alef build"),
"a real type error must never tell the reader to rebuild toolchains: {result:?}"
);
}
#[test]
fn finalize_result_still_flags_a_real_missing_module_as_unresolved_dependency() {
let validator = crate::snippets::validators::typescript::TypeScriptValidator;
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let outcome = ValidationOutcome {
status: SnippetStatus::Fail,
message: Some("snippet.ts(1,1): error TS2307: Cannot find module 'widgets'.".to_string()),
duration_ms: 5,
};
let result = finalize_result(
&typescript_snippet(),
&validator,
&config,
None,
ValidationLevel::Compile,
outcome,
);
assert_eq!(result.status, SnippetStatus::Unavailable, "got: {result:?}");
assert!(
result.unresolved_dependency,
"a genuinely unresolved module must still be flagged: {result:?}"
);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains("Cannot find module 'widgets'"),
"the original diagnostic must still be included, not replaced: {result:?}"
);
}
#[test]
fn finalize_result_with_no_session_names_the_no_session_cause_not_alef_build() {
let validator = crate::snippets::validators::typescript::TypeScriptValidator;
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let outcome = ValidationOutcome {
status: SnippetStatus::Fail,
message: Some("snippet.ts(1,1): error TS2307: Cannot find module 'widgets'.".to_string()),
duration_ms: 5,
};
let result = finalize_result(
&typescript_snippet(),
&validator,
&config,
None,
ValidationLevel::Compile,
outcome,
);
let message = result.message.as_deref().unwrap_or_default();
assert!(
message.contains(super::dependency_reclassification::NO_SESSION_CONFIGURED_PHRASE),
"a session-less reclassification must name the real cause: {message}"
);
assert!(
!message.contains("run `alef build` first"),
"a session-less reclassification must not send the reader to rebuild artifacts a \
session-less run could never see: {message}"
);
}
#[test]
fn finalize_result_with_a_configured_session_keeps_the_ordering_message() {
let validator = crate::snippets::validators::typescript::TypeScriptValidator;
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let outcome = ValidationOutcome {
status: SnippetStatus::Fail,
message: Some("snippet.ts(1,1): error TS2307: Cannot find module 'widgets'.".to_string()),
duration_ms: 5,
};
let session = crate::snippets::session::ValidationSession {
language: crate::snippets::types::Language::TypeScript,
working_directory: std::path::PathBuf::from("."),
manifest: None,
fingerprint: "fixture".into(),
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
};
let result = finalize_result(
&typescript_snippet(),
&validator,
&config,
Some(&session),
ValidationLevel::Compile,
outcome,
);
let message = result.message.as_deref().unwrap_or_default();
assert!(
message.contains("run `alef build` first"),
"a session-backed reclassification must still point at the real remedy: {message}"
);
assert!(
!message.contains(super::dependency_reclassification::NO_SESSION_CONFIGURED_PHRASE),
"a configured session must never be reported as missing: {message}"
);
}
fn two_same_language_sessions(node: &std::path::Path, wasm: &std::path::Path) -> HashMap<String, SessionSpec> {
let spec = |working_directory: &std::path::Path| SessionSpec {
language: crate::snippets::types::Language::TypeScript,
working_directory: working_directory.to_path_buf(),
manifest: None,
before: Vec::new(),
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
};
HashMap::from([("typescript".to_string(), spec(node)), ("wasm".to_string(), spec(wasm))])
}
#[test]
fn an_ambiguous_session_claim_is_a_real_error_on_the_fail_fast_path() {
let node = tempfile::tempdir().expect("node session directory");
let wasm = tempfile::tempdir().expect("wasm session directory");
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(UnreachableValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
sessions: two_same_language_sessions(node.path(), wasm.path()),
fail_fast: true,
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.total, 1);
assert_eq!(
summary.failed, 0,
"an ambiguous claim is a configuration gap, not a snippet defect"
);
assert_eq!(summary.errors, 1);
assert!(
summary.has_failures(),
"an ambiguous claim must fail every run, not just --strict"
);
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Error);
let message = outcome.message.as_deref().unwrap_or_default();
assert!(
message.contains("typescript"),
"message must name every candidate session: {message}"
);
assert!(
message.contains("wasm"),
"message must name every candidate session: {message}"
);
assert!(
message.contains("target:"),
"message must tell the reader how to resolve it: {message}"
);
}
#[test]
fn an_ambiguous_session_claim_is_a_real_error_on_the_parallel_path() {
let node = tempfile::tempdir().expect("node session directory");
let wasm = tempfile::tempdir().expect("wasm session directory");
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(UnreachableValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
sessions: two_same_language_sessions(node.path(), wasm.path()),
fail_fast: false,
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.total, 1);
assert_eq!(summary.failed, 0);
assert_eq!(summary.errors, 1);
assert!(summary.has_failures());
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Error, "got: {outcome:?}");
let message = outcome.message.as_deref().unwrap_or_default();
assert!(
message.contains("typescript"),
"message must name every candidate session: {message}"
);
assert!(
message.contains("wasm"),
"message must name every candidate session: {message}"
);
assert!(
message.contains("target:"),
"message must tell the reader how to resolve it: {message}"
);
}
#[test]
fn a_single_differently_named_session_still_claims_a_target_less_snippet() {
let directory = tempfile::tempdir().expect("session directory");
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(GenuinelyBrokenValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
sessions: HashMap::from([(
"node".to_string(),
SessionSpec {
language: crate::snippets::types::Language::TypeScript,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: Vec::new(),
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
},
)]),
..RunnerConfig::default()
};
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
let outcome = &summary.results[0];
assert_eq!(outcome.status, SnippetStatus::Error, "got: {outcome:?}");
assert!(
outcome
.message
.as_deref()
.unwrap_or_default()
.contains("binding-aware sessions"),
"the snippet must have reached the validator carrying the `node` session, not `None`: {outcome:?}"
);
}
#[test]
fn a_before_hook_is_bounded_by_its_own_budget_when_one_is_configured() {
let directory = tempfile::tempdir().expect("session directory");
let mut registry = ValidatorRegistry::new();
registry.register(Box::new(UnreachableValidator));
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
timeout_secs: 600,
before_timeout_secs: Some(1),
sessions: timing_out_session(directory.path()),
fail_fast: false,
..RunnerConfig::default()
};
let started = std::time::Instant::now();
let summary = run_validation(&[typescript_snippet()], ®istry, &config).expect("validation completes");
assert_eq!(summary.unresolved_dependency, 1);
assert_eq!(summary.results[0].status, SnippetStatus::Unavailable);
assert!(
started.elapsed() < std::time::Duration::from_secs(60),
"the hook must be cut off at its own 1s budget, not at the 600s snippet budget"
);
}
#[test]
fn an_unset_hook_budget_falls_back_to_the_snippet_timeout() {
let shared = RunnerConfig {
timeout_secs: 42,
..RunnerConfig::default()
};
let separate = RunnerConfig {
timeout_secs: 42,
before_timeout_secs: Some(900),
..RunnerConfig::default()
};
assert_eq!(shared.resolved_before_timeout_secs(), 42);
assert_eq!(separate.resolved_before_timeout_secs(), 900);
}
fn go_snippet() -> Snippet {
Snippet {
id: None,
path: "example.md".into(),
language: crate::snippets::types::Language::Go,
title: None,
code: "package main\n\nfunc main() {}\n".into(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: "example.md".into(),
line: 1,
block_index: 0,
},
}
}
fn go_session_fixture() -> crate::snippets::session::ValidationSession {
crate::snippets::session::ValidationSession {
language: crate::snippets::types::Language::Go,
working_directory: std::path::PathBuf::from("."),
manifest: None,
fingerprint: "fixture".into(),
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
}
}
#[test]
fn finalize_result_buckets_every_captured_go_linker_missing_library_form_as_unavailable() {
let cases: &[(&str, &str)] = &[
(
"GNU ld (Linux gcc/clang default)",
"# example.test/module\n/usr/bin/ld: cannot find -lsample_ffi: No such file or directory\n\
collect2: error: ld returned 1 exit status\n",
),
(
"Apple ld (macOS clang default)",
"# example.test/module\nld: library not found for -lsample_ffi\n\
clang: error: linker command failed with exit code 1 (use -v to see invocation)\n",
),
(
"LLVM lld (-fuse-ld=lld)",
"# example.test/module\nld.lld: error: unable to find library -lsample_ffi\n\
clang: error: linker command failed with exit code 1 (use -v to see invocation)\n",
),
];
let validator = crate::snippets::validators::go::GoValidator;
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let session = go_session_fixture();
for (platform, raw_output) in cases {
let outcome = ValidationOutcome {
status: SnippetStatus::Fail,
message: Some((*raw_output).to_string()),
duration_ms: 5,
};
let result = finalize_result(
&go_snippet(),
&validator,
&config,
Some(&session),
ValidationLevel::Compile,
outcome,
);
assert_eq!(
result.status,
SnippetStatus::Unavailable,
"{platform}: a missing linked library must bucket as unavailable, not a real failure: {result:?}"
);
assert!(
result.unresolved_dependency,
"{platform}: must be flagged as an unresolved dependency: {result:?}"
);
let message = result.message.as_deref().unwrap_or_default();
assert!(
message.contains("run `alef build` first"),
"{platform}: a session-backed reclassification must point at the real remedy: {message}"
);
assert!(
message.contains("sample_ffi"),
"{platform}: the original linker diagnostic must survive in the message: {message}"
);
}
}
#[test]
fn finalize_result_keeps_a_real_go_compile_error_as_fail_not_unavailable() {
let validator = crate::snippets::validators::go::GoValidator;
let config = RunnerConfig {
level: ValidationLevel::Compile,
cache_dir: None,
..RunnerConfig::default()
};
let diagnostic = "./snippet.go:3:10: cannot use \"text\" (untyped string constant) as int value in assignment";
let outcome = ValidationOutcome {
status: SnippetStatus::Fail,
message: Some(diagnostic.to_string()),
duration_ms: 5,
};
let result = finalize_result(
&go_snippet(),
&validator,
&config,
Some(&go_session_fixture()),
ValidationLevel::Compile,
outcome,
);
assert_eq!(result.status, SnippetStatus::Fail, "got: {result:?}");
assert!(
!result.unresolved_dependency,
"a real compile error must not be flagged as a dependency gap"
);
assert_eq!(
result.message.as_deref(),
Some(diagnostic),
"a real compile error's message must stay the compiler's own text verbatim: {result:?}"
);
assert!(
!result.message.as_deref().unwrap_or_default().contains("alef build"),
"a real compile error must never tell the reader to rebuild toolchains: {result:?}"
);
}