use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use lex_extension_host::{
resolve_namespace_with, FetchError, Fetcher, FetcherRegistry, ParsedUri, ResolverCache,
};
struct RecordingHttpsFetcher {
requests: Arc<Mutex<Vec<ParsedUri>>>,
request_count: Arc<AtomicUsize>,
}
impl Fetcher for RecordingHttpsFetcher {
fn fetch(&self, uri: &ParsedUri, dest: &Path) -> Result<(), FetchError> {
self.request_count.fetch_add(1, Ordering::SeqCst);
self.requests.lock().unwrap().push(uri.clone());
std::fs::write(dest.join("schema.yaml"), FIXTURE_SCHEMA)?;
Ok(())
}
fn schemes(&self) -> &'static [&'static str] {
&["https"]
}
}
struct RecordingGitFetcher {
requests: Arc<Mutex<Vec<ParsedUri>>>,
}
impl Fetcher for RecordingGitFetcher {
fn fetch(&self, uri: &ParsedUri, dest: &Path) -> Result<(), FetchError> {
self.requests.lock().unwrap().push(uri.clone());
std::fs::write(dest.join("schema.yaml"), FIXTURE_SCHEMA)?;
Ok(())
}
fn schemes(&self) -> &'static [&'static str] {
&["git"]
}
}
const FIXTURE_SCHEMA: &[u8] = b"schema_version: 1\nlabel: github.stub\n";
#[test]
fn github_tap_resolves_via_https_default() {
let requests = Arc::new(Mutex::new(Vec::new()));
let request_count = Arc::new(AtomicUsize::new(0));
let fetcher = Arc::new(RecordingHttpsFetcher {
requests: Arc::clone(&requests),
request_count: Arc::clone(&request_count),
});
let mut registry = FetcherRegistry::new();
registry.register(fetcher);
let cache_root = tempfile::tempdir().expect("tempdir");
let cache = ResolverCache::new(cache_root.path()).expect("cache root");
let workspace = tempfile::tempdir().expect("workspace");
let resolved = resolve_namespace_with(
"github:acme/lex-labels",
workspace.path(),
®istry,
&cache,
)
.expect("resolve github tap");
assert!(
resolved.schema_dir.starts_with(cache_root.path()),
"github tap should resolve into the cache, got: {}",
resolved.schema_dir.display()
);
assert_eq!(
std::fs::read(resolved.schema_dir.join("schema.yaml")).unwrap(),
FIXTURE_SCHEMA,
"fetched body should match stub fetcher's output"
);
let seen = requests.lock().unwrap();
assert_eq!(seen.len(), 1, "fetcher should see exactly one request");
let uri = &seen[0];
assert_eq!(uri.scheme, "https");
assert_eq!(
uri.body, "//api.github.com/repos/acme/lex-labels/tarball/HEAD",
"github template should expand to the tarball-API URL"
);
assert_eq!(
uri.original, "github:acme/lex-labels",
"expanded URI should preserve the original `github:` form for diagnostics"
);
}
#[test]
fn github_tap_cache_reuse_on_second_resolve() {
let requests = Arc::new(Mutex::new(Vec::new()));
let request_count = Arc::new(AtomicUsize::new(0));
let fetcher = Arc::new(RecordingHttpsFetcher {
requests: Arc::clone(&requests),
request_count: Arc::clone(&request_count),
});
let mut registry = FetcherRegistry::new();
registry.register(fetcher);
let cache_root = tempfile::tempdir().expect("tempdir");
let cache = ResolverCache::new(cache_root.path()).expect("cache root");
let workspace = tempfile::tempdir().expect("workspace");
let r1 = resolve_namespace_with(
"github:acme/lex-labels#v1.0.0",
workspace.path(),
®istry,
&cache,
)
.expect("first resolve");
let r2 = resolve_namespace_with(
"github:acme/lex-labels#v1.0.0",
workspace.path(),
®istry,
&cache,
)
.expect("second resolve");
assert_eq!(
r1.schema_dir, r2.schema_dir,
"same URI should resolve to the same cache dir"
);
assert_eq!(
request_count.load(Ordering::SeqCst),
1,
"fetcher should be called once; second resolve must hit cache"
);
}
#[test]
fn github_via_git_resolves_via_local_bare_repo() {
let requests = Arc::new(Mutex::new(Vec::new()));
let fetcher = Arc::new(RecordingGitFetcher {
requests: Arc::clone(&requests),
});
let mut registry = FetcherRegistry::new();
registry.register(fetcher);
let cache_root = tempfile::tempdir().expect("tempdir");
let cache = ResolverCache::new(cache_root.path()).expect("cache root");
let workspace = tempfile::tempdir().expect("workspace");
let resolved = resolve_namespace_with(
"github:acme/lex-labels?via=git",
workspace.path(),
®istry,
&cache,
)
.expect("resolve github via=git");
assert!(
resolved.schema_dir.starts_with(cache_root.path()),
"via=git tap should resolve into the cache, got: {}",
resolved.schema_dir.display()
);
let seen = requests.lock().unwrap();
assert_eq!(seen.len(), 1, "git fetcher should see exactly one request");
let uri = &seen[0];
assert_eq!(uri.scheme, "git", "via=git must dispatch to the git scheme");
assert_eq!(
uri.body, "https://github.com/acme/lex-labels.git",
"via=git body should be the github HTTPS clone URL the user's credential helper handles"
);
assert_eq!(
uri.via, None,
"via is a one-shot routing knob; templates must not propagate it downstream"
);
assert_eq!(
uri.original, "github:acme/lex-labels?via=git",
"expanded URI should preserve the original tap form for diagnostics"
);
}
#[test]
fn github_malformed_tap_with_many_slashes_produces_sensible_url() {
let requests = Arc::new(Mutex::new(Vec::new()));
let request_count = Arc::new(AtomicUsize::new(0));
let fetcher = Arc::new(RecordingHttpsFetcher {
requests: Arc::clone(&requests),
request_count: Arc::clone(&request_count),
});
let mut registry = FetcherRegistry::new();
registry.register(fetcher);
let cache_root = tempfile::tempdir().expect("tempdir");
let cache = ResolverCache::new(cache_root.path()).expect("cache root");
let workspace = tempfile::tempdir().expect("workspace");
let resolved = resolve_namespace_with(
"github:weird/with/many/slashes",
workspace.path(),
®istry,
&cache,
)
.expect("malformed-shape tap should still expand and dispatch");
assert!(resolved.schema_dir.starts_with(cache_root.path()));
let seen = requests.lock().unwrap();
assert_eq!(seen.len(), 1);
let uri = &seen[0];
assert_eq!(uri.scheme, "https");
assert_eq!(
uri.body, "//api.github.com/repos/weird/with/many/slashes/tarball/HEAD",
"extra slashes pass through into the tarball URL verbatim"
);
}
#[test]
fn github_via_unknown_value_surfaces_as_parse_error() {
let requests = Arc::new(Mutex::new(Vec::new()));
let request_count = Arc::new(AtomicUsize::new(0));
let fetcher = Arc::new(RecordingHttpsFetcher {
requests: Arc::clone(&requests),
request_count: Arc::clone(&request_count),
});
let mut registry = FetcherRegistry::new();
registry.register(fetcher);
let cache_root = tempfile::tempdir().expect("tempdir");
let cache = ResolverCache::new(cache_root.path()).expect("cache root");
let workspace = tempfile::tempdir().expect("workspace");
let err = resolve_namespace_with(
"github:acme/lex-labels?via=ftp",
workspace.path(),
®istry,
&cache,
)
.expect_err("unknown via= value must error");
let _ = err; assert_eq!(
request_count.load(Ordering::SeqCst),
0,
"unsupported via= must not reach the fetcher"
);
}