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
1 change: 1 addition & 0 deletions .alfonso/release-notes/next/batch-3-startup-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- AFT no longer stops answering for several seconds after a restart when its database holds a long history of shell commands.
1 change: 1 addition & 0 deletions .alfonso/release-notes/next/rust-1-100-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rust diagnostics: completed Cargo checks can be saved and restored with Rust 1.100's new intermediate-artifact layout, including projects with build scripts. Rust 1.99's layout remains supported.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"slice_class": "non_ranking",
"targeted_mechanism": "none",
"kind": "harness",
"fixtures": ["harness-goldens"],
"note": "Follow-up to post-0.64 batch 3: startup held-task recovery reads in bounded indexed pages, database sections of the permission-card and shells paths move off the subc runtime thread, and Rust 1.100 Cargo layout support for completed Rust checks and native test launchers. No ranking-fence file changes; search is untouched."
}
1 change: 1 addition & 0 deletions crates/aft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ windows-sys = { version = "0.61", features = [
] }

[dev-dependencies]
cortexkit-test-support = "=0.1.14"
graphql-parser = "0.4.1"
cortexkit-role-tool-provider-conformance = "=0.15.6"
cortexkit-role-tool-provider = { version = "=0.15.5", features = ["test-vectors"] }
Expand Down
82 changes: 73 additions & 9 deletions crates/aft/src/bash_background/secrets_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,26 +597,48 @@ fn secret_bash_windows_refuses_all_profiles_without_a_job() {
}

#[cfg(unix)]
fn native_launcher(root: &Path) -> impl Drop {
// The test executable runs from `<profile>/deps/`, and `aft` is built
// into `<profile>/`. With Cargo's `build.build-dir` set, test
// executables move under the build folder while binaries stay in the
// target folder, so the same profile folder there is looked in too.
let executable = std::env::current_exe().unwrap();
let profile_dir = executable.parent().unwrap().parent().unwrap();
fn native_launcher_candidates(
executable: &Path,
provided: Option<std::path::PathBuf>,
target: Option<std::ffi::OsString>,
manifest: Option<std::ffi::OsString>,
) -> Vec<std::path::PathBuf> {
if let Some(provided) = provided {
return vec![provided];
}
// Nextest only supplies BIN_EXE variables to integration tests and benches,
// not library tests. Locate the profile above Cargo's old `deps` or new
// `build/<package>/<unit>` intermediate tree in the extracted archive.
let Some(profile_dir) = cortexkit_test_support::cargo_profile_dir_of(executable) else {
return Vec::new();
};
let profile = profile_dir.file_name().unwrap();
let mut candidates = vec![profile_dir.join("aft")];
if let Some(target) = std::env::var_os("CARGO_TARGET_DIR") {
if let Some(target) = target {
candidates.push(std::path::PathBuf::from(target).join(profile).join("aft"));
}
if let Some(manifest) = std::env::var_os("CARGO_MANIFEST_DIR") {
if let Some(manifest) = manifest {
candidates.push(
std::path::PathBuf::from(manifest)
.join("../../target")
.join(profile)
.join("aft"),
);
}
candidates
}

#[cfg(unix)]
fn native_launcher(root: &Path) -> impl Drop {
let candidates = native_launcher_candidates(
&std::env::current_exe().unwrap(),
std::env::var_os("AFT_TEST_AFT_BINARY")
.or_else(|| std::env::var_os("NEXTEST_BIN_EXE_aft"))
.or_else(|| std::env::var_os("CARGO_BIN_EXE_aft"))
.map(std::path::PathBuf::from),
std::env::var_os("CARGO_TARGET_DIR"),
std::env::var_os("CARGO_MANIFEST_DIR"),
);
let launcher = candidates
.iter()
.find(|path| path.is_file())
Expand All @@ -627,6 +649,48 @@ fn native_launcher(root: &Path) -> impl Drop {
crate::sandbox_spawn::install_sandbox_launcher_for_test(root, launcher)
}

#[cfg(unix)]
#[test]
fn native_launcher_candidates_support_both_cargo_layouts() {
for executable in [
"/archive/target/debug/deps/agent_file_tools-hash",
"/archive/target/debug/build/agent-file-tools/hash/aft",
"/archive/target/debug/build/agent-file-tools/hash/out/aft",
] {
let candidates = native_launcher_candidates(
Path::new(executable),
None,
Some("/custom-target".into()),
Some("/checkout/crates/aft".into()),
);
assert_eq!(
candidates,
vec![
std::path::PathBuf::from("/archive/target/debug/aft"),
std::path::PathBuf::from("/custom-target/debug/aft"),
std::path::PathBuf::from("/checkout/crates/aft/../../target/debug/aft"),
],
"{executable}"
);
}
}

#[cfg(unix)]
#[test]
fn native_launcher_candidates_respect_explicit_binary_even_if_missing() {
let binary = std::path::PathBuf::from("/missing/explicit-aft");
assert_eq!(
native_launcher_candidates(
Path::new("/archive/target/debug/build/agent-file-tools/hash/aft"),
Some(binary.clone()),
Some("/custom-target".into()),
None,
),
vec![binary],
"a missing explicit binary must fail rather than use a stale fallback"
);
}

#[cfg(unix)]
#[test]
fn secret_bash_real_plan_precedence_and_untrusted_request_scope() {
Expand Down
6 changes: 6 additions & 0 deletions crates/aft/src/list_surfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ pub static EXCLUSIONS: &[ExclusionEntry] = &[
location_or_primitive: "SQL LIMIT, PER_TICK (4)",
reason: "Keyset pagination bounds background lifecycle probes, not permissions.list; every live grant remains listed and later maintenance ticks continue after the saved cursor.",
},
ExclusionEntry {
file: "subc/held_task_recovery.rs",
enclosing_item: "sessions",
location_or_primitive: "RECOVERY_CHUNK (64), SQL LIMIT, keyset pagination",
reason: "Internal startup recovery exhausts every candidate source in index-seeking pages; the chunk bounds one shared database mutex hold, not an agent-visible result list.",
},
ExclusionEntry {
file: "subc/shells/runtime.rs",
enclosing_item: "list, project_row, output",
Expand Down
126 changes: 94 additions & 32 deletions crates/aft/src/lsp/completed_rust_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,43 +886,59 @@ impl Runtime {
profiles.push(triple.path().join("release/build"));
}
}
let prefix = format!("{}-", config.get("name")?.as_str()?);
let package_name = config.get("name")?.as_str()?;
let prefix = format!("{package_name}-");
let mut declared = false;
for profile in profiles {
let mut entries = match fs::read_dir(profile) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return None,
};
loop {
expired(deadline)?;
let Some(entry) = entries.next() else {
break;
// Cargo 1.100 groups build units by package instead of keeping
// build-script runs in package-hash directories. Their captured
// stdout moved from `output` to `<package>/<unit>/run/stdout`.
let mut directories = vec![(profile, false)];
while let Some((directory, package_units)) = directories.pop() {
let mut entries = match fs::read_dir(directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return None,
};
let entry = entry.ok()?;
if !entry.file_name().to_str()?.starts_with(&prefix) {
continue;
}
let output = entry.path().join("output");
if !output.is_file() {
continue;
}
let bytes = read_bounded(&output, deadline)?;
for line in std::str::from_utf8(&bytes).ok()?.lines() {
loop {
expired(deadline)?;
if let Some(key) = line
.strip_prefix("cargo:rerun-if-env-changed=")
.or_else(|| line.strip_prefix("cargo::rerun-if-env-changed="))
{
declared = true;
env_keys.push(key.to_string());
let Some(entry) = entries.next() else {
break;
};
let entry = entry.ok()?;
let name = entry.file_name();
let name = name.to_str()?;
if !package_units && name == package_name {
directories.push((entry.path(), true));
continue;
}
if let Some(path) = line
.strip_prefix("cargo:rerun-if-changed=")
.or_else(|| line.strip_prefix("cargo::rerun-if-changed="))
{
declared = true;
outputs.push(canonical(&package.join(path))?);
let output = if package_units {
entry.path().join("run/stdout")
} else if name.starts_with(&prefix) {
entry.path().join("output")
} else {
continue;
};
if !output.is_file() {
continue;
}
let bytes = read_bounded(&output, deadline)?;
for line in std::str::from_utf8(&bytes).ok()?.lines() {
expired(deadline)?;
if let Some(key) = line
.strip_prefix("cargo:rerun-if-env-changed=")
.or_else(|| line.strip_prefix("cargo::rerun-if-env-changed="))
{
declared = true;
env_keys.push(key.to_string());
}
if let Some(path) = line
.strip_prefix("cargo:rerun-if-changed=")
.or_else(|| line.strip_prefix("cargo::rerun-if-changed="))
{
declared = true;
outputs.push(canonical(&package.join(path))?);
}
}
}
}
Expand Down Expand Up @@ -1894,6 +1910,52 @@ mod tests {
cache.validated_with_runtime("v1".into(), Instant::now() + BUDGET)
}

#[test]
fn build_inputs_support_legacy_and_unit_layouts() {
for profile in ["debug", "release", "some-target/debug"] {
for output in ["member-control/output", "member/control/run/stdout"] {
let (_temp, mut cache) = fixture();
cache.runtime.env.insert(
"CARGO_TARGET_DIR".into(),
cache.root.join("target").to_string_lossy().into_owned(),
);
cache.runtime.options = Some(serde_json::json!({"cargo": {"targetDir": true}}));
write(&cache.root, "member/input.txt", "build input");
let inputs = &cache.saved.as_ref().unwrap().fingerprint;
let collect = || {
cache
.runtime
.build_inputs(&cache.root, inputs, Instant::now() + BUDGET)
};
assert!(
collect().is_none(),
"missing build output must not certify inputs"
);
write(
&cache.root,
&format!("target/rust-analyzer/{profile}/build/{output}"),
"cargo:rerun-if-changed=input.txt\ncargo::rerun-if-env-changed=BUILD_INPUT\n",
);
// Other packages must not add declarations to this package's
// certificate, even when their names share its prefix.
write(
&cache.root,
&format!(
"target/rust-analyzer/{profile}/build/member-other/control/run/stdout"
),
"cargo:rerun-if-changed=nonexistent.txt\n",
);
let (paths, env) = collect().expect("discover declared build inputs");
assert_eq!(
paths,
vec![canonical(&cache.root.join("member/input.txt")).unwrap()],
"{profile}/{output}"
);
assert_eq!(env, vec!["BUILD_INPUT"], "{profile}/{output}");
}
}
}

#[test]
fn next_write_removes_record_for_deleted_checkout() {
let (temp, cache) = fixture();
Expand Down
Loading