Skip to content

Commit c55974a

Browse files
committed
fixes and better test
1 parent 94f96af commit c55974a

4 files changed

Lines changed: 352 additions & 10 deletions

File tree

agent-dev

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ echo "💡 After session: merge-overlay will run automatically"
223223
echo "💡 To restore home: rm -rf $AI_HOME"
224224
echo ""
225225

226+
exit_code=0
226227
bwrap \
227228
`# ===== System directories (read-only) =====` \
228229
--ro-bind /usr /usr \
@@ -302,7 +303,7 @@ bwrap \
302303
--unsetenv GPG_AGENT_INFO \
303304
\
304305
`# ===== Execute command (defaults to claude) =====` \
305-
"${@:-claude}"
306+
"${@:-claude}" || exit_code=$?
306307

307308
# After bwrap exits, run merge-overlay to review changes
308309
echo ""
@@ -317,3 +318,5 @@ if [[ -n "$(ls -A "$OVERLAY_UPPER" 2>/dev/null)" ]]; then
317318
else
318319
echo "✓ No changes in overlay - nothing to merge"
319320
fi
321+
322+
exit $exit_code

merge-overlay

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,21 @@ get_file_status() {
6262
local repo_file="$2"
6363

6464
# Check if it's a whiteout file (deletion marker in overlayfs)
65+
# Old style: .wh. prefix
6566
if [[ "$(basename "$overlay_file")" =~ ^\.wh\. ]]; then
6667
echo "deleted"
6768
return
6869
fi
6970

71+
# Modern overlayfs: character device with major/minor 0/0
72+
if [[ -c "$overlay_file" ]]; then
73+
local dev_info=$(stat -c "%t %T" "$overlay_file" 2>/dev/null || echo "")
74+
if [[ "$dev_info" == "0 0" ]]; then
75+
echo "deleted"
76+
return
77+
fi
78+
fi
79+
7080
# Check if it's a directory
7181
if [[ -d "$overlay_file" ]]; then
7282
echo "directory"
@@ -84,6 +94,14 @@ get_file_status() {
8494
# Get the original filename from whiteout marker
8595
get_whiteout_target() {
8696
local whiteout_file="$1"
97+
98+
# For character device whiteouts (modern overlayfs), filename is already correct
99+
if [[ -c "$whiteout_file" ]]; then
100+
echo "$whiteout_file"
101+
return
102+
fi
103+
104+
# For .wh. prefix whiteouts (old style), strip the prefix
87105
echo "$(dirname "$whiteout_file")/$(basename "$whiteout_file" | sed 's/^\.wh\.//')"
88106
}
89107

@@ -261,17 +279,45 @@ echo " N - Skip all remaining"
261279
echo ""
262280

263281
# Find all files in overlay (including hidden files, excluding . and ..)
264-
# We need to handle whiteout files and regular files
282+
# We need to handle whiteout files (both .wh. prefix and char devices 0/0) and regular files
265283
FILES=()
266284
while IFS= read -r -d '' file; do
267285
rel_path="${file#$OVERLAY_UPPER/}"
268286
FILES+=("$rel_path")
269-
done < <(find "$OVERLAY_UPPER" -type f -print0 | sort -z)
287+
done < <(find "$OVERLAY_UPPER" \( -type f -o -type c \) -print0 | sort -z)
270288

271289
TOTAL=${#FILES[@]}
272290
echo "Found $TOTAL file(s) to review"
273291
echo ""
274292

293+
# List all files with their statuses
294+
echo "Files to review:"
295+
echo "----------------------------------------"
296+
for rel_path in "${FILES[@]}"; do
297+
overlay_file="$OVERLAY_UPPER/$rel_path"
298+
repo_file="$REPO_DIR/$rel_path"
299+
status=$(get_file_status "$overlay_file" "$repo_file")
300+
301+
case "$status" in
302+
deleted)
303+
original_name=$(get_whiteout_target "$overlay_file")
304+
original_rel_path="${original_name#$OVERLAY_UPPER/}"
305+
echo -e "${RED}[DELETED]${NC} $original_rel_path"
306+
;;
307+
modified)
308+
echo -e "${YELLOW}[MODIFIED]${NC} $rel_path"
309+
;;
310+
new)
311+
echo -e "${BLUE}[NEW]${NC} $rel_path"
312+
;;
313+
directory)
314+
# Skip directories in the list
315+
;;
316+
esac
317+
done
318+
echo "----------------------------------------"
319+
echo ""
320+
275321
# Process each file
276322
BATCH_MODE=""
277323
for rel_path in "${FILES[@]}"; do
@@ -318,8 +364,8 @@ for rel_path in "${FILES[@]}"; do
318364
esac
319365
else
320366
# Interactive mode
321-
process_file "$rel_path"
322-
ret=$?
367+
ret=0
368+
process_file "$rel_path" || ret=$?
323369

324370
case $ret in
325371
2)

tests/test-agent-dev.expect

Lines changed: 190 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,13 @@ proc test_file_creation_isolation {} {
193193

194194
catch wait result
195195

196-
# Check file was in overlay (before we discarded it)
197-
# Note: file will be gone after discard, so we can't verify this way
198-
# The test passes if merge-overlay detected and handled it
196+
# Verify overlay is now empty (confirms only one file was created and discarded)
197+
set overlay_files [glob -nocomplain -directory $overlay_upper *]
198+
if {[llength $overlay_files] == 0} {
199+
log_success "Overlay empty after discard (only one file was created)"
200+
} else {
201+
log_failure "Overlay not empty - unexpected files remain: $overlay_files"
202+
}
199203

200204
if {![file exists "$repo_dir/$test_file"]} {
201205
log_success "Original repository not modified"
@@ -207,7 +211,7 @@ proc test_file_creation_isolation {} {
207211

208212
# Test 3: Protected directory access
209213
proc test_protected_directories {} {
210-
global repo_dir
214+
global repo_dir overlay_upper
211215

212216
start_test "Protected directories are read-only"
213217

@@ -244,6 +248,14 @@ proc test_protected_directories {} {
244248

245249
catch wait
246250

251+
# Verify overlay is empty (protected dirs shouldn't create overlay files)
252+
set overlay_files [glob -nocomplain -directory $overlay_upper *]
253+
if {[llength $overlay_files] == 0} {
254+
log_success "Overlay empty (protected directory write blocked)"
255+
} else {
256+
log_failure "Overlay not empty - unexpected files: $overlay_files"
257+
}
258+
247259
# Verify .git/config wasn't actually modified
248260
if {[catch {exec grep -c "# test" "$repo_dir/.git/config"} result] || $result eq "0"} {
249261
log_success ".git/config remained unmodified"
@@ -425,7 +437,179 @@ proc test_network_access {} {
425437
catch wait
426438
}
427439

428-
# Test 8: Overlay cleanup on empty session
440+
# Test 8: Comprehensive multi-file merge workflow
441+
proc test_multi_file_merge_workflow {} {
442+
global repo_dir overlay_upper
443+
444+
start_test "Multi-file merge: NEW, MODIFIED, DELETED with diff/accept/skip/discard"
445+
446+
# Pre-create files: one to modify, one to delete
447+
set existing_file "to-be-deleted.txt"
448+
set modify_file "to-be-modified.txt"
449+
exec sh -c "echo 'will be deleted' > $repo_dir/$existing_file"
450+
exec sh -c "echo 'original content' > $repo_dir/$modify_file"
451+
452+
# Create multiple files, modify one, delete one in a single session
453+
set timestamp [clock seconds]
454+
spawn ./agent-dev bash -c "
455+
echo 'content for file1' > merge-test1-$timestamp.txt
456+
echo 'content for file2' > merge-test2-$timestamp.txt
457+
echo 'content for file3' > merge-test3-$timestamp.txt
458+
echo 'modified content' > $modify_file
459+
rm $existing_file
460+
echo 'Session complete: 3 new, 1 modified, 1 deleted'
461+
"
462+
463+
expect {
464+
"Session complete: 3 new, 1 modified, 1 deleted" {
465+
log_success "Multiple changes made in sandbox (3 NEW, 1 MODIFIED, 1 DELETED)"
466+
}
467+
timeout {
468+
log_failure "Timeout creating changes in sandbox"
469+
expect eof
470+
catch wait
471+
return
472+
}
473+
}
474+
475+
# Verify overlay has changes (3 new files + 1 modified + deletion marker)
476+
# Note: Modern overlayfs uses character devices (0/0) for deletions, not .wh. files
477+
set overlay_items [glob -nocomplain -directory $overlay_upper *]
478+
479+
if {[llength $overlay_items] >= 4} {
480+
log_success "Overlay contains changes (found [llength $overlay_items] items)"
481+
} else {
482+
log_failure "Expected at least 4 items in overlay, found [llength $overlay_items]"
483+
}
484+
485+
# Handle all merge-overlay interactions
486+
# Use a simpler approach - just respond to each prompt as it comes
487+
expect {
488+
-re "Found (\\d+) file" {
489+
set num_files $expect_out(1,string)
490+
log_info "Merge-overlay will process $num_files files"
491+
}
492+
timeout {
493+
log_failure "Timeout waiting for file count"
494+
}
495+
}
496+
497+
# Handle each file interactively
498+
while {1} {
499+
expect {
500+
-re "NEW.*merge-test1-$timestamp\\.txt.*Action" {
501+
send "d\r"
502+
log_success "Requested diff for NEW file 1"
503+
exp_continue
504+
}
505+
-re "File does not exist in repository.*Action" {
506+
send "y\r"
507+
log_success "Diff warning shown, accepting NEW file 1"
508+
exp_continue
509+
}
510+
-re "✓ Accepted" {
511+
exp_continue
512+
}
513+
-re "NEW.*merge-test2-$timestamp\\.txt.*Action" {
514+
send "n\r"
515+
log_success "Skipping NEW file 2"
516+
exp_continue
517+
}
518+
-re "○ Skipped" {
519+
exp_continue
520+
}
521+
-re "NEW.*merge-test3-$timestamp\\.txt.*Action" {
522+
send "r\r"
523+
log_success "Discarding NEW file 3"
524+
exp_continue
525+
}
526+
-re "✓ Discarded" {
527+
exp_continue
528+
}
529+
-re "MODIFIED.*$modify_file.*Action" {
530+
send "d\r"
531+
log_success "Requested diff for MODIFIED file"
532+
exp_continue
533+
}
534+
-re "Diff:.*$modify_file.*Action" {
535+
send "y\r"
536+
log_success "Diff shown, accepting MODIFIED file"
537+
exp_continue
538+
}
539+
-re "DELETED.*$existing_file.*Action" {
540+
send "y\r"
541+
log_success "Accept DELETED file"
542+
exp_continue
543+
}
544+
-re "(File does not exist|already.*deleted).*Action" {
545+
send "y\r"
546+
log_success "Diff handled, accepting DELETED file"
547+
exp_continue
548+
}
549+
-re "✓ Deleted from repository" {
550+
exp_continue
551+
}
552+
eof {
553+
log_success "Merge-overlay completed all files"
554+
break
555+
}
556+
timeout {
557+
log_failure "Timeout during merge-overlay interaction"
558+
break
559+
}
560+
}
561+
}
562+
563+
catch wait
564+
565+
# Verify final state
566+
# File 1 should be in repo
567+
if {[file exists "$repo_dir/merge-test1-$timestamp.txt"]} {
568+
log_success "Accepted NEW file correctly in repository"
569+
} else {
570+
log_failure "Accepted NEW file not found in repository"
571+
}
572+
573+
# File 2 should still be in overlay
574+
if {[file exists "$overlay_upper/merge-test2-$timestamp.txt"]} {
575+
log_success "Skipped NEW file correctly remains in overlay"
576+
} else {
577+
log_failure "Skipped NEW file not found in overlay"
578+
}
579+
580+
# File 3 should not be in repo or overlay
581+
if {![file exists "$repo_dir/merge-test3-$timestamp.txt"] && ![file exists "$overlay_upper/merge-test3-$timestamp.txt"]} {
582+
log_success "Discarded NEW file correctly removed"
583+
} else {
584+
log_failure "Discarded NEW file found where it shouldn't be"
585+
}
586+
587+
# Modified file should have new content
588+
if {[file exists "$repo_dir/$modify_file"]} {
589+
set content [exec cat "$repo_dir/$modify_file"]
590+
if {$content eq "modified content"} {
591+
log_success "MODIFIED file has updated content in repository"
592+
} else {
593+
log_failure "MODIFIED file content incorrect: $content"
594+
}
595+
} else {
596+
log_failure "MODIFIED file not found in repository"
597+
}
598+
599+
# Deleted file should not exist in repo
600+
if {![file exists "$repo_dir/$existing_file"]} {
601+
log_success "DELETED file correctly removed from repository"
602+
} else {
603+
log_failure "DELETED file still exists in repository"
604+
}
605+
606+
# Cleanup: remove skipped file from overlay and test files from repo
607+
exec rm -f "$overlay_upper/merge-test2-$timestamp.txt"
608+
exec rm -f "$repo_dir/merge-test1-$timestamp.txt"
609+
exec rm -f "$repo_dir/$modify_file"
610+
}
611+
612+
# Test 9: Overlay cleanup on empty session
429613
proc test_empty_overlay_handling {} {
430614
global overlay_upper
431615

@@ -483,6 +667,7 @@ proc run_all_tests {} {
483667
test_home_isolation
484668
test_working_directory
485669
test_network_access
670+
test_multi_file_merge_workflow
486671
test_empty_overlay_handling
487672

488673
cleanup_test_environment

0 commit comments

Comments
 (0)