Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/command/rename_detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,22 @@ impl WorktreeReadBudget {
mod tests {
use super::*;

/// Make `root` discoverable as a repository and step the process cwd into
/// it, returning the guard that restores the cwd.
///
/// Deliberately NOT a full `libra init`: the worktree read path only needs
/// repository DISCOVERY to succeed (`util::working_dir`, for the LFS
/// attribute lookup), and a terminal common storage is a gitdir carrying
/// the repository database. Standing up a real database here would drag a
/// connection pool into a test about byte accounting.
fn repo_fixture(root: &Path) -> crate::utils::test::ChangeDirGuard {
let gitdir = root.join(crate::utils::util::ROOT_DIR);
std::fs::create_dir_all(gitdir.join("objects")).expect("create object store");
std::fs::write(gitdir.join(crate::utils::util::DATABASE), b"")
.expect("create repository db");
crate::utils::test::ChangeDirGuard::new(root)
}

fn regular(evidence: BlobEvidence, size: u64) -> BlobRef {
BlobRef {
kind: BlobKind::Regular,
Expand Down Expand Up @@ -2288,6 +2304,12 @@ mod tests {
const SHRUNK: u64 = 4;
const GROWTH: u64 = 4096;
let dir = tempfile::tempdir().expect("tempdir");
// The read path classifies LFS through `attribute_state_for_path`,
// which resolves `working_dir()` — infallibly, from the PROCESS cwd,
// on the pooled io thread. Outside a repository that panics the
// worker and the caller only ever sees the resulting `IoTimeout`,
// so the fixture has to be a repository.
let _repo = repo_fixture(dir.path());
let path = dir.path().join("grows.txt");
std::fs::write(&path, vec![b'a'; (SHRUNK + GROWTH) as usize])
.expect("write the full-size file");
Expand Down Expand Up @@ -2456,6 +2478,11 @@ mod tests {
use std::time::Duration;

let dir = tempfile::tempdir().expect("tempdir");
// See `worktree_total_charges_bytes_read_not_stale_stat`: the LFS
// classification resolves `working_dir()` on the io thread, so a
// repo-less fixture fails with `IoTimeout` no matter what the seams
// are doing — which is precisely the outcome this test denies.
let _repo = repo_fixture(dir.path());
let path = dir.path().join("fast.txt");
std::fs::write(&path, b"content").expect("write fixture");

Expand All @@ -2466,8 +2493,21 @@ mod tests {
std::env::set_var("LIBRA_TEST_SLOW_WORKTREE_READ_MS", "5000");
std::env::set_var("LIBRA_TEST_SLOW_LFS_ATTRIBUTES_MS", "5000");
}
let mut budget = WorktreeReadBudget::new(1024, 1024, 8, Duration::from_millis(1500));
let outcome = budget.read_worktree_blob(&path);
// A jammed I/O pool and a fired seam both surface as `IoTimeout`, and
// the pool is process-global: the seam tests above abandon reads that
// keep sleeping in a worker, and their slots outlive them. So drain
// the pool first and retry — with the seams inert one attempt
// succeeds as soon as a worker is free, while a 5s seam that really
// did fire would blow the 1.5s batch on EVERY attempt.
let mut outcome = ContentOutcome::Skipped(SkipReason::IoTimeout);
for _ in 0..8 {
crate::command::status_probe::wait_for_idle_io_pool();
let mut budget = WorktreeReadBudget::new(1024, 1024, 8, Duration::from_millis(1500));
outcome = budget.read_worktree_blob(&path);
if matches!(outcome, ContentOutcome::Content(_)) {
break;
}
}
unsafe {
std::env::remove_var("LIBRA_TEST_SLOW_WORKTREE_STAT_MS");
std::env::remove_var("LIBRA_TEST_SLOW_WORKTREE_READ_MS");
Expand Down
18 changes: 18 additions & 0 deletions src/command/status_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ static IO_POOL: std::sync::OnceLock<std::sync::Arc<IoWorkerPool>> = std::sync::O
static IO_BUSY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static IO_WORKERS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);

/// Block until every pooled slot has been released.
///
/// The deadline tests deliberately ABANDON reads: the caller gives up at its
/// deadline while the worker keeps sleeping inside the armed seam, and the
/// slot stays busy until that sleep ends — outliving the test that started
/// it. A later test whose contract is "this must NOT time out" therefore has
/// to start from an idle pool, or it measures the previous test's leftover
/// delay and fails for a reason that has nothing to do with what it asserts.
#[cfg(test)]
pub(crate) fn wait_for_idle_io_pool() {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while IO_BUSY.load(std::sync::atomic::Ordering::SeqCst) > 0
&& std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(20));
}
}

/// Run one blocking filesystem operation with a wall-clock deadline.
///
/// A hung syscall cannot be interrupted in safe Rust, so the operation runs
Expand Down
36 changes: 27 additions & 9 deletions src/internal/ai/hooks/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,13 @@ mod tests {
run_builtin_migrations(&conn)
.await
.expect("run_builtin_migrations");
// `libra init` writes `libra.repoid` as part of bootstrap, and every
// ingest path resolves its `CaptureScope` through `RepoIdentity` — a
// fixture without it is not a repository this code will write to, it
// is one with corrupt metadata.
ConfigKv::set_with_conn(&conn, "libra.repoid", "repo-ingest-fixture", false)
.await
.expect("seed libra.repoid");
(dir, conn)
}

Expand Down Expand Up @@ -3730,8 +3737,13 @@ mod tests {
)
.await
.expect_err("stale hook must not resurrect an erased session");
// The tombstone is now enforced inside the scope-claim upsert's WHERE
// clause rather than by a separate pre-check, so the refusal surfaces
// through the claim diagnostic. The row-count assertion below is the
// contract that matters; this one only pins that erasure is named as a
// cause rather than the write silently succeeding.
assert!(
err.to_string().contains("anti-resurrection tombstone"),
err.to_string().contains("could not be claimed") && err.to_string().contains("erased"),
"unexpected error: {err:#}"
);

Expand Down Expand Up @@ -4153,14 +4165,20 @@ mod tests {

#[tokio::test]
async fn ingest_fails_loud_when_table_missing() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("noschema.db");
std::fs::File::create(&path).expect("touch sqlite file");
let url = format!("sqlite://{}", path.display());
let mut opts = ConnectOptions::new(url);
opts.sqlx_logging(false);
let conn = Database::connect(opts).await.expect("connect");
// intentionally NOT calling run_builtin_migrations.
// A fully bootstrapped repository with exactly ONE table removed. A
// schema-less database would fail earlier and for a different reason
// (`config_kv` is read first, to resolve the capture scope), which
// would leave the claim this test actually makes — that a missing
// `agent_session` is reported loudly and by name — untested.
let (_dir, conn) = ingest_fresh_conn().await;
let backend = conn.get_database_backend();
let _: ExecResult = conn
.execute_raw(Statement::from_string(
backend,
"DROP TABLE agent_session".to_string(),
))
.await
.expect("drop agent_session");

let payload = ingest_envelope("SessionStart", "S-bare", json!({}));
let err = ingest_agent_traces_payload(
Expand Down
5 changes: 4 additions & 1 deletion src/internal/ai/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1591,10 +1591,13 @@ mod tests {

use axum::extract::connect_info::MockConnectInfo;

/// What the adapter recorded, as `(message body, command id)`.
type Submitted = Arc<Mutex<Vec<(String, Option<String>)>>>;

#[derive(Clone)]
struct CommandIdAdapter {
session: Arc<CodeUiSession>,
submitted: Arc<Mutex<Vec<(String, Option<String>)>>>,
submitted: Submitted,
}

#[async_trait::async_trait]
Expand Down
47 changes: 44 additions & 3 deletions src/internal/worktree_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,37 @@ mod tests {
#[test]
#[serial_test::serial]
fn a_pinned_scope_survives_a_cwd_change() {
// A REAL repository, because `pin_scope_for_test` resolves its paths
// through `RequestScope::resolve` and installs nothing when the
// workdir is not inside one — pinning the ambient cwd only appeared to
// work when another test had parked it in someone else's fixture.
//
// Built BEFORE the cwd lock below: `setup_with_new_libra_in` takes and
// releases that lock itself, and running a whole `libra init` while
// holding it would block every other fixture in the suite for the
// duration.
let repo = tempfile::tempdir().expect("repo");
{
let _cd = crate::utils::test::ChangeDirGuard::new(repo.path());
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime")
.block_on(crate::utils::test::setup_with_new_libra_in(repo.path()));
}

// Moving the process cwd is not this test's business alone: every
// `ChangeDirGuard` in the suite reads the same one. `#[serial]` only
// orders this against other `#[serial]` tests, so hold the cwd lock
// the guard uses — otherwise the `set_current_dir` below yanks the
// directory out from under whatever repository fixture is mid-flight.
let _cwd_lock = crate::utils::test::cwd_lock_guard();
let original = std::env::current_dir().expect("cwd");
let elsewhere = std::env::temp_dir();

let _pin = WorktreeScope::pin_scope_for_test(
WorktreeScope::Linked("wt-pinned".to_string()),
original.clone(),
repo.path().to_path_buf(),
);
assert!(WorktreeScope::request_scope_is_pinned());
assert_eq!(WorktreeScope::for_request().storage_key(), "wt-pinned");
Expand Down Expand Up @@ -432,6 +457,10 @@ mod tests {
.expect("runtime")
.block_on(crate::utils::test::setup_with_new_libra_in(repo.path()));
}
// See `a_pinned_scope_survives_a_cwd_change`: the raw `set_current_dir`
// below is process-wide, so it has to hold `ChangeDirGuard`'s lock —
// taken only now, so the `libra init` above does not run under it.
let _cwd_lock = crate::utils::test::cwd_lock_guard();
let original = std::env::current_dir().expect("cwd");

// Pin a SUBDIRECTORY, which is what a command invoked from one does.
Expand Down Expand Up @@ -483,6 +512,10 @@ mod tests {
.expect("runtime")
.block_on(crate::utils::test::setup_with_new_libra_in(repo.path()));
}
// See `a_pinned_scope_survives_a_cwd_change`: the raw `set_current_dir`
// below is process-wide, so it has to hold `ChangeDirGuard`'s lock —
// taken only now, so the `libra init` above does not run under it.
let _cwd_lock = crate::utils::test::cwd_lock_guard();
let original = std::env::current_dir().expect("cwd");
let canonical_repo =
std::fs::canonicalize(repo.path()).unwrap_or_else(|_| repo.path().to_path_buf());
Expand Down Expand Up @@ -523,11 +556,15 @@ mod tests {
async fn the_request_database_follows_the_pin_not_the_cwd() {
let repo_a = tempfile::tempdir().expect("repo a");
let repo_b = tempfile::tempdir().expect("repo b");
let original = std::env::current_dir().expect("cwd");
for repo in [repo_a.path(), repo_b.path()] {
let _cd = crate::utils::test::ChangeDirGuard::new(repo);
crate::utils::test::setup_with_new_libra_in(repo).await;
}
// See `a_pinned_scope_survives_a_cwd_change`: the raw `set_current_dir`
// below is process-wide, so it has to hold `ChangeDirGuard`'s lock —
// taken only now, so the two `libra init`s above do not run under it.
let _cwd_lock = crate::utils::test::cwd_lock_guard();
let original = std::env::current_dir().expect("cwd");

let _pin = WorktreeScope::pin_request_scope(repo_a.path().to_path_buf());
// The cwd is repository B; the pin is repository A.
Expand Down Expand Up @@ -576,11 +613,15 @@ mod tests {
let outer = tempfile::tempdir().expect("the enclosing repository");
let ambient = tempfile::tempdir().expect("the repository the cwd is in");
let nowhere = tempfile::tempdir().expect("not a repository");
let original = std::env::current_dir().expect("cwd");
for repo in [outer.path(), ambient.path()] {
let _cd = crate::utils::test::ChangeDirGuard::new(repo);
crate::utils::test::setup_with_new_libra_in(repo).await;
}
// See `a_pinned_scope_survives_a_cwd_change`: the raw `set_current_dir`
// below is process-wide, so it has to hold `ChangeDirGuard`'s lock —
// taken only now, so the two `libra init`s above do not run under it.
let _cwd_lock = crate::utils::test::cwd_lock_guard();
let original = std::env::current_dir().expect("cwd");

let _outer_pin = WorktreeScope::pin_request_scope(outer.path().to_path_buf());
std::env::set_current_dir(ambient.path()).expect("move the cwd");
Expand Down
5 changes: 5 additions & 0 deletions src/utils/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ mod tests {
let linked = root.path().join("linked");
let linked_gitdir = linked.join(".libra");
fs::create_dir_all(common.join("objects")).expect("create shared object store");
// The commondir target must look like TERMINAL common storage
// (`util::is_terminal_common_storage`), which an object store alone
// does not: a real main gitdir also carries the repository database,
// and without it the pointer is refused as corruption.
fs::write(common.join(crate::utils::util::DATABASE), b"").expect("create repository db");
fs::create_dir_all(&linked_gitdir).expect("create linked worktree gitdir");
fs::write(
linked_gitdir.join("commondir"),
Expand Down
23 changes: 23 additions & 0 deletions tests/data/ai_semantic/rust/sample.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Fixture for `tests/ai_semantic_rust_test.rs`. Line numbers are pinned by
//! that test: `Widget::label` must start on line 10, `make_widget` on 15.

pub struct Widget {
name: String,
}

impl Widget {
// Reads the widget's label.
fn label(&self) -> &str {
&self.name
}
fn new(name: &str) -> Self { Self { name: name.to_string() } }
}
pub fn make_widget(name: &str) -> Widget {
Widget::new(name)
}

fn handle() {}

mod nested {
pub fn handle() {}
}
32 changes: 32 additions & 0 deletions tests/data/ai_semantic/rust/tools_sample.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Fixture for `tests/ai_semantic_tools_test.rs` (semantic tool handlers).
//!
//! Kept separate from `sample.rs`, whose line numbers are pinned by
//! `tests/ai_semantic_rust_test.rs` — this file's contents matter
//! (Widget::new + its call site, an ambiguous `handle`), not its line
//! numbers.

pub struct Widget {
name: String,
}

impl Widget {
pub fn new(name: String) -> Self {
Widget { name }
}

// Reads the widget's label.
fn label(&self) -> &str {
&self.name
}
}

pub fn make_widget(name: &str) -> Widget {
let name = name.to_string();
Widget::new(name)
}

fn handle() {}

mod nested {
pub fn handle() {}
}
Loading