Files
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

405 lines
13 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Integration test for cgroup memory-high OOM handling.
//!
//! **Must be run on Linux with cgroupv2** and sufficient permissions to create
//! child cgroups (typically root, or a user-session cgroup with delegation).
//!
//! Run with:
//! ```bash
//! # On a Linux machine (as root or with cgroup delegation):
//! cargo test -p kigi-tools --test cgroup_memory_test -- --ignored --nocapture
//!
//! # If you need root:
//! sudo -E cargo test -p kigi-tools --test cgroup_memory_test -- --ignored --nocapture
//! ```
//!
//! The cgroup-dependent tests (15) are `#[ignore]`d by default so they don't
//! run in CI where cgroup delegation is typically unavailable. Test 6 (no-config)
//! always runs.
//!
//! The tests exercise:
//! 1. A command that stays under the memory limit → exits normally (exit 0)
//! 2. A command that exceeds memory.high → killed with exit 137, signal "oom"
//! 3. The session (backend) survives an OOM and can run another command after
//! 4. Background tasks are also killed on OOM
//! 5. A gradual allocator that slowly ramps up past the limit
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::computer::local::cgroup::{CgroupMemoryConfig, PROCESS_OOM_EXIT_CODE};
use kigi_tools::computer::types::{TerminalBackend, TerminalRunRequest, TerminalRunResult};
use kigi_tools::notification::types::ToolNotificationHandle;
/// Small memory limit for testing: 32 MiB high, 32 MiB headroom (64 MiB hard max).
fn test_memory_config() -> CgroupMemoryConfig {
CgroupMemoryConfig {
memory_high_bytes: 32 * 1024 * 1024,
headroom_bytes: 32 * 1024 * 1024,
}
}
fn make_request(command: &str, timeout_secs: u64) -> TerminalRunRequest {
let output_file = std::env::temp_dir().join(format!(
"cgroup-test-{}-{}.out",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
TerminalRunRequest {
command: command.to_string(),
working_directory: PathBuf::from("/tmp"),
env: HashMap::new(),
timeout: Duration::from_secs(timeout_secs),
output_byte_limit: 1024 * 1024,
output_file,
notification_handle: ToolNotificationHandle::noop(),
tool_call_id: format!("cgroup-test-{}", uuid::Uuid::now_v7()),
display_command: None,
auto_background_on_timeout: false,
foreground_block_budget: None,
kind: Default::default(),
owner_session_id: None,
}
}
fn is_linux_with_cgroupv2() -> bool {
#[cfg(target_os = "linux")]
{
std::path::Path::new("/sys/fs/cgroup/cgroup.controllers").exists()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
fn can_create_cgroups() -> bool {
if !is_linux_with_cgroupv2() {
return false;
}
// Try reading our own cgroup path — if this works, we can probably create children
#[cfg(target_os = "linux")]
{
if let Ok(contents) = std::fs::read_to_string("/proc/self/cgroup") {
for line in contents.lines() {
if let Some(path) = line.strip_prefix("0::") {
let cgroup_dir = std::path::PathBuf::from(format!("/sys/fs/cgroup{}", path));
let subtree = cgroup_dir.join("cgroup.subtree_control");
return subtree.exists();
}
}
}
false
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
fn skip_unless_cgroup() {
if !can_create_cgroups() {
eprintln!(
"\n╔══════════════════════════════════════════════════════════════╗\n\
║ SKIPPED: cgroupv2 not available or insufficient perms. ║\n\
║ Run on Linux as root or with cgroup delegation. ║\n\
╚══════════════════════════════════════════════════════════════╝\n"
);
}
}
fn print_result(label: &str, result: &TerminalRunResult) {
let output_preview = if result.combined_output.len() > 200 {
format!("{}…", &result.combined_output[..200])
} else {
result.combined_output.clone()
};
eprintln!(
"\n── {label} ──\n exit_code: {:?}\n signal: {:?}\n timed_out: {}\n truncated: {}\n output: {:?}\n",
result.exit_code,
result.signal,
result.timed_out,
result.truncated,
output_preview.trim(),
);
}
#[tokio::test]
#[ignore = "requires Linux cgroupv2 with delegation — run with: cargo test --test cgroup_memory_test -- --ignored --nocapture"]
async fn test_under_limit_exits_normally() {
skip_unless_cgroup();
if !can_create_cgroups() {
return;
}
eprintln!("\n=== Test: under_limit_exits_normally ===");
let backend = LocalTerminalBackend::with_memory_limit(test_memory_config());
// Give actor time to initialize cgroup
tokio::time::sleep(Duration::from_millis(200)).await;
let result = backend
.run(make_request(
"echo 'hello from cgroup'; cat /proc/self/cgroup",
10,
))
.await
.expect("command should succeed");
print_result("Under limit", &result);
assert_eq!(result.exit_code, Some(0), "Expected exit code 0");
assert!(
result.combined_output.contains("hello from cgroup"),
"Output should contain our echo"
);
assert_ne!(
result.signal.as_deref(),
Some("oom"),
"Should NOT be OOM-killed"
);
eprintln!("✅ PASSED: under_limit_exits_normally");
}
#[tokio::test]
#[ignore = "requires Linux cgroupv2 with delegation"]
async fn test_over_limit_gets_oom_killed() {
skip_unless_cgroup();
if !can_create_cgroups() {
return;
}
eprintln!("\n=== Test: over_limit_gets_oom_killed ===");
let backend = LocalTerminalBackend::with_memory_limit(test_memory_config());
tokio::time::sleep(Duration::from_millis(200)).await;
// Allocate 128 MiB in Python — well above the 32 MiB high / 64 MiB max limits.
let alloc_cmd = r#"python3 -c "
import sys
print('Allocating 128 MiB...', flush=True)
data = bytearray(128 * 1024 * 1024)
print('Allocation succeeded (should not reach here)', flush=True)
""#;
let result = backend
.run(make_request(alloc_cmd, 30))
.await
.expect("command should return a result (even if killed)");
print_result("Over limit", &result);
// The process should be killed — either by our monitor (exit 137 + signal "oom")
// or by the kernel hard OOM killer (exit 137 / signal 9).
let killed_by_memory = result.exit_code == Some(PROCESS_OOM_EXIT_CODE)
|| result.signal.as_deref() == Some("oom")
|| result
.signal
.as_ref()
.is_some_and(|s| s.contains("signal 9"));
assert!(
killed_by_memory,
"Expected OOM kill (exit 137 or signal 9/oom), got exit_code={:?} signal={:?}",
result.exit_code, result.signal
);
assert!(
result.combined_output.contains("Allocating 128 MiB"),
"Output before OOM should be captured"
);
eprintln!("✅ PASSED: over_limit_gets_oom_killed");
}
#[tokio::test]
#[ignore = "requires Linux cgroupv2 with delegation"]
async fn test_session_survives_oom() {
skip_unless_cgroup();
if !can_create_cgroups() {
return;
}
eprintln!("\n=== Test: session_survives_oom ===");
let backend = LocalTerminalBackend::with_memory_limit(test_memory_config());
tokio::time::sleep(Duration::from_millis(200)).await;
let oom_cmd = r#"python3 -c "data = bytearray(128 * 1024 * 1024)""#;
let oom_result = backend
.run(make_request(oom_cmd, 30))
.await
.expect("should return result even on OOM");
print_result("OOM command", &oom_result);
// Small delay so cgroup memory is reclaimed
tokio::time::sleep(Duration::from_millis(500)).await;
let ok_result = backend
.run(make_request("echo 'alive after OOM'", 10))
.await
.expect("post-OOM command should succeed");
print_result("After OOM", &ok_result);
assert_eq!(
ok_result.exit_code,
Some(0),
"Post-OOM command should exit 0"
);
assert!(
ok_result.combined_output.contains("alive after OOM"),
"Post-OOM output should contain our echo"
);
eprintln!("✅ PASSED: session_survives_oom");
}
#[tokio::test]
#[ignore = "requires Linux cgroupv2 with delegation"]
async fn test_background_task_oom() {
skip_unless_cgroup();
if !can_create_cgroups() {
return;
}
eprintln!("\n=== Test: background_task_oom ===");
let backend = LocalTerminalBackend::with_memory_limit(test_memory_config());
tokio::time::sleep(Duration::from_millis(200)).await;
let alloc_cmd = r#"python3 -c "
import time
print('BG: allocating...', flush=True)
time.sleep(0.5)
data = bytearray(128 * 1024 * 1024)
print('BG: done (should not reach here)', flush=True)
time.sleep(60)
""#;
let handle = backend
.run_background(make_request(alloc_cmd, 60))
.await
.expect("background spawn should succeed");
eprintln!(" Background task_id: {}", handle.task_id);
let snapshot = backend
.wait_for_completion(&handle.task_id, Some(Duration::from_secs(30)))
.await;
if let Some(snap) = &snapshot {
eprintln!(
" BG result: completed={} exit_code={:?} signal={:?} output={:?}",
snap.completed,
snap.exit_code,
snap.signal,
&snap.output[..snap.output.len().min(200)]
);
assert!(snap.completed, "Background task should have completed");
let killed_by_memory = snap.exit_code == Some(PROCESS_OOM_EXIT_CODE)
|| snap.signal.as_deref() == Some("oom")
|| snap.signal.as_ref().is_some_and(|s| s.contains("signal 9"));
assert!(
killed_by_memory,
"Background task should be OOM-killed, got exit_code={:?} signal={:?}",
snap.exit_code, snap.signal
);
} else {
panic!("Expected a snapshot for the background task");
}
eprintln!("✅ PASSED: background_task_oom");
}
/// Slow ramp past the limit exercises the inotify monitor catching the
/// memory.high event rather than the kernel's hard memory.max kill.
#[tokio::test]
#[ignore = "requires Linux cgroupv2 with delegation"]
async fn test_gradual_allocation_oom() {
skip_unless_cgroup();
if !can_create_cgroups() {
return;
}
eprintln!("\n=== Test: gradual_allocation_oom ===");
let backend = LocalTerminalBackend::with_memory_limit(test_memory_config());
tokio::time::sleep(Duration::from_millis(200)).await;
// Allocate in 1 MiB chunks with a small delay — slowly ramps past 32 MiB.
let gradual_cmd = r#"python3 -c "
import time, sys
chunks = []
for i in range(128):
chunks.append(bytearray(1024 * 1024)) # 1 MiB per chunk
print(f'Allocated {i+1} MiB', flush=True)
time.sleep(0.05)
print('Finished all allocations (should not reach here)', flush=True)
""#;
let result = backend
.run(make_request(gradual_cmd, 30))
.await
.expect("should return result");
print_result("Gradual allocation", &result);
let killed_by_memory = result.exit_code == Some(PROCESS_OOM_EXIT_CODE)
|| result.signal.as_deref() == Some("oom")
|| result
.signal
.as_ref()
.is_some_and(|s| s.contains("signal 9"));
assert!(
killed_by_memory,
"Expected OOM kill for gradual allocator, got exit_code={:?} signal={:?}",
result.exit_code, result.signal
);
assert!(
result.combined_output.contains("Allocated"),
"Should see some allocation progress before kill"
);
assert!(
!result.combined_output.contains("Finished all allocations"),
"Should have been killed before finishing"
);
eprintln!("✅ PASSED: gradual_allocation_oom");
}
#[tokio::test]
async fn test_no_config_no_enforcement() {
eprintln!("\n=== Test: no_config_no_enforcement ===");
let backend = LocalTerminalBackend::new();
tokio::time::sleep(Duration::from_millis(200)).await;
// Allocate 64 MiB — would be killed with a 32 MiB limit, but should succeed here
let alloc_cmd = r#"python3 -c "
data = bytearray(64 * 1024 * 1024)
print('Allocated 64 MiB without limits')
""#;
let result = backend
.run(make_request(alloc_cmd, 10))
.await
.expect("command should succeed without limits");
print_result("No enforcement", &result);
assert_eq!(result.exit_code, Some(0), "Should exit 0 without limits");
assert!(
result.combined_output.contains("Allocated 64 MiB"),
"Allocation should succeed without limits"
);
eprintln!("✅ PASSED: no_config_no_enforcement");
}