fix(ci): restore green baseline (#162)

* fix(ci): restore green baseline

* test(cli): normalize purge preview paths

* test(cli): avoid temp path spelling in purge preview
This commit is contained in:
Fabio Akita
2026-07-10 20:43:04 -03:00
committed by GitHub
parent 7bf0b1c041
commit 2c4231e535
5 changed files with 108 additions and 12 deletions
+9
View File
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Upgraded `crossbeam-epoch` to the RUSTSEC-2026-0204 fixed release.
- Fixed the Docker wrapper on macOS with rootless Docker so host-config
commands (`install-mcp`, `install-hooks`, `install-instructions`, and
related setup/removal commands) keep the `-u 0:0` mapping needed to write
bind-mounted agent configuration as the invoking host user.
- Stabilized the Windows uninstall purge-preview regression test against
verbatim temp-path spelling.
## [1.11.1] - 2026-07-09
### Fixed
Generated
+2 -2
View File
@@ -696,9 +696,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
+12 -4
View File
@@ -319,8 +319,11 @@ esac
# write to a host bind mount: other commands only touch the /data named
# volume, which isn't host-visible and doesn't have this problem, so leave
# their UID mapping alone.
if [ "${WRITES_HOST_FILES}" -eq 1 ] \
&& "${DOCKER}" info --format '{{.SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'; then
ROOTLESS_DOCKER=0
if "${DOCKER}" info --format '{{.SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'; then
ROOTLESS_DOCKER=1
fi
if [ "${WRITES_HOST_FILES}" -eq 1 ] && [ "${ROOTLESS_DOCKER}" -eq 1 ]; then
USER_ARGS=(-u 0:0)
fi
@@ -350,8 +353,13 @@ case "$(uname -s 2>/dev/null || true)" in
# create log files or write to the data dir, crashing with
# "Permission denied" in the rolling file appender.
# Omitting -u lets the container run as its default (uid 1000)
# which matches the volume owner.
USER_ARGS=()
# which matches the volume owner. Keep the earlier rootless-Docker
# exception for commands that write host config: under rootless Docker,
# UID 0 maps back to the invoking host user and is the only mapping that
# can write bind-mounted agent config reliably.
if ! { [ "${ROOTLESS_DOCKER}" -eq 1 ] && [ "${WRITES_HOST_FILES}" -eq 1 ]; }; then
USER_ARGS=()
fi
;;
esac
+75 -4
View File
@@ -211,9 +211,19 @@ fn macos_wrapper_routes_urls_by_real_subcommand() {
// logging argv to a file for `run` (read back by the test).
#[cfg(unix)]
fn run_wrapper_with_fake_docker(args: &[&str], docker_info_stdout: &str) -> String {
run_wrapper_with_fake_docker_and_uname(args, docker_info_stdout, None)
}
#[cfg(unix)]
fn run_wrapper_with_fake_docker_and_uname(
args: &[&str],
docker_info_stdout: &str,
uname_stdout: Option<&str>,
) -> String {
let tmp = tempfile::tempdir().unwrap();
let docker_args = tmp.path().join("docker-args.txt");
let docker = tmp.path().join("docker");
let uname = tmp.path().join("uname");
std::fs::write(
&docker,
format!(
@@ -226,22 +236,44 @@ fn run_wrapper_with_fake_docker(args: &[&str], docker_info_stdout: &str) -> Stri
),
)
.unwrap();
if let Some(uname_stdout) = uname_stdout {
std::fs::write(
&uname,
format!("#!/usr/bin/env bash\nprintf '{}\\n'\n", uname_stdout),
)
.unwrap();
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&docker, std::fs::Permissions::from_mode(0o755)).unwrap();
if uname_stdout.is_some() {
std::fs::set_permissions(&uname, std::fs::Permissions::from_mode(0o755)).unwrap();
}
}
let path = if uname_stdout.is_some() {
Some(format!(
"{}:{}",
shell_path(tmp.path()),
std::env::var("PATH").unwrap_or_default()
))
} else {
None
};
let mut command = shell_script_command(&repo_root().join("bin/ai-memory"));
let output = command
command
.args(args)
.env("AI_MEMORY_DOCKER", shell_path(&docker))
.env("AI_MEMORY_NO_VERSION_CHECK", "1")
.env("AI_MEMORY_DATA_VOLUME", "test-ai-memory-data")
.env("HOME", shell_path(tmp.path()))
.env_remove("AI_MEMORY_SERVER_URL")
.output()
.unwrap();
.env_remove("AI_MEMORY_SERVER_URL");
if let Some(path) = path {
command.env("PATH", path);
}
let output = command.output().unwrap();
assert!(
output.status.success(),
"wrapper failed: stdout={} stderr={}",
@@ -251,6 +283,15 @@ fn run_wrapper_with_fake_docker(args: &[&str], docker_info_stdout: &str) -> Stri
std::fs::read_to_string(docker_args).unwrap()
}
#[cfg(unix)]
fn run_wrapper_with_fake_rootless_docker_on_fake_macos(args: &[&str]) -> String {
run_wrapper_with_fake_docker_and_uname(
args,
"[name=apparmor name=seccomp,profile=default name=rootless]",
Some("Darwin"),
)
}
#[cfg(unix)]
#[test]
fn rootless_docker_uses_root_uid_only_for_host_config_commands() {
@@ -284,6 +325,36 @@ fn rootless_docker_uses_root_uid_only_for_host_config_commands() {
);
}
#[cfg(unix)]
#[test]
fn fake_macos_rootless_docker_keeps_root_uid_for_host_config_commands() {
let args = run_wrapper_with_fake_rootless_docker_on_fake_macos(&["install-mcp"]);
assert!(
args.contains("-u\n0:0"),
"macOS rootless Docker still needs uid 0 for host config writes; got {args}"
);
let args = run_wrapper_with_fake_rootless_docker_on_fake_macos(&["status"]);
assert!(
!args.contains("-u\n0:0"),
"macOS thin-client commands should keep Docker Desktop's default uid; got {args}"
);
}
#[cfg(unix)]
#[test]
fn fake_macos_rootful_docker_keeps_default_uid_for_host_config_commands() {
let args = run_wrapper_with_fake_docker_and_uname(
&["install-mcp"],
"[name=seccomp,profile=default]",
Some("Darwin"),
);
assert!(
!args.contains("-u\n0:0") && !args.contains("-u\n"),
"macOS rootful Docker should keep Docker Desktop's default uid; got {args}"
);
}
#[cfg(unix)]
#[test]
fn rootful_docker_keeps_host_uid_for_host_config_commands() {
+10 -2
View File
@@ -39,7 +39,11 @@ fn command_with_home(home: &Path) -> Command {
}
fn normalize_path_text(value: impl AsRef<str>) -> String {
value.as_ref().replace('\\', "/")
value
.as_ref()
.replace('\\', "/")
.replace("//?/UNC/", "//")
.replace("//?/", "")
}
fn run_uninstall(project: &Path, home: &Path, args: &[&str]) -> std::process::Output {
@@ -719,10 +723,14 @@ fn uninstall_dry_run_previews_purge() {
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("would purge"), "stdout was: {stdout}");
let normalized_stdout = normalize_path_text(&stdout);
for sub in ["wiki", "db", "raw"] {
let p = data.path().join(sub);
let expected_suffix = format!("/{sub}");
assert!(
stdout.contains(&p.display().to_string()),
normalized_stdout
.lines()
.any(|line| line.starts_with("would purge ") && line.ends_with(&expected_suffix)),
"missing {sub} in: {stdout}"
);
// Dry-run must not delete.