From d4f84f7d2951027f2a6f15c2c9f579ca8cf1968d Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 6 Jul 2026 18:33:08 +0800 Subject: [PATCH] fix: GNU-first stat in file_mtime() to unbreak GNU/Linux CI (#245) idd-tree-lock file_mtime() tried BSD `stat -f %m` first. On GNU/Linux `-f` is --file-system, so it leaks a multi-line filesystem block into holder_is_live()`s freshness arithmetic; a fresh empty lock is then wrongly reclaimed instead of held. That broke the f8 fixture only on the Linux CI runner (green on macOS), leaving main red. Swap to GNU-first `stat -c %Y || stat -f %m`: on Linux the first form succeeds and short-circuits (no fs garbage); on BSD/macOS `-c` is illegal -> clean rc=1 empty stdout -> falls through to `-f %m`. Add regression fixture f11 that PATH-shims a GNU-semantics stat so the Linux-only failure is now reproducible on any host (RED before, GREEN after). Full suite 21/21. Refs #245 --- .../issue-driven-dev/scripts/idd-tree-lock.sh | 7 ++++- .../scripts/tests/idd-tree-lock/test.sh | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/plugins/issue-driven-dev/scripts/idd-tree-lock.sh b/plugins/issue-driven-dev/scripts/idd-tree-lock.sh index 3fc4680..ae9ab8f 100755 --- a/plugins/issue-driven-dev/scripts/idd-tree-lock.sh +++ b/plugins/issue-driven-dev/scripts/idd-tree-lock.sh @@ -62,7 +62,12 @@ LOCK="$LOCK_PARENT/tree-lock" now_epoch() { date +%s; } now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; } -file_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null; } +# GNU-first: `stat -c %Y` is GNU "format" (mtime); on Linux it succeeds and the +# `||` short-circuits, so GNU's `-f`=--file-system multi-line output never leaks +# into the mtime. On BSD/macOS `-c` is illegal → clean usage error to stderr, no +# stdout (rc=1) → falls through to `-f %m` (BSD "format"). BSD-first breaks the +# reverse way on Linux and reclaims fresh locks (#245). Regression: test.sh f11. +file_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; } field() { [ -f "$LOCK" ] && sed -n "s/^$1=//p" "$LOCK" | head -1; } valid_pid() { printf '%s' "$1" | grep -qE '^[0-9]+$' && [ "$1" -gt 0 ] 2>/dev/null; } diff --git a/plugins/issue-driven-dev/scripts/tests/idd-tree-lock/test.sh b/plugins/issue-driven-dev/scripts/tests/idd-tree-lock/test.sh index cb5e76a..7ac0032 100755 --- a/plugins/issue-driven-dev/scripts/tests/idd-tree-lock/test.sh +++ b/plugins/issue-driven-dev/scripts/tests/idd-tree-lock/test.sh @@ -103,4 +103,31 @@ assert_eq "f10b exactly one holder after reclaim" "new" "$(lockpid_holder "$R")" require "f10c no orphan reclaim mutex left" bash -c "[ ! -d '$R/.claude/.idd/tree-lock.reclaim' ]" rm -rf "$R" +# ── Fixture 11 (#245 REGRESSION): file_mtime() must resolve mtime under GNU stat +# semantics. `stat -f %m` is BSD "format"; on GNU/Linux `-f` = --file-system, so +# a BSD-first chain leaks a multi-line filesystem block into holder_is_live()'s +# freshness arithmetic — a FRESH empty lock is then wrongly reclaimed (f8 passes +# on macOS, fails on the Linux CI runner). We shim a GNU-semantics `stat` onto +# PATH so the Linux-only failure reproduces on ANY host: RED while file_mtime +# tries `-f` first, GREEN once it tries `-c %Y` first. ── +R="$(mk_repo)"; mkdir -p "$R/.claude/.idd" +: > "$R/.claude/.idd/tree-lock" # empty + fresh = mid-acquire window (as f8) +SHIMDIR="$(mktemp -d)" +cat > "$SHIMDIR/stat" <<'SHIM' +#!/bin/sh +# GNU-coreutils `stat` semantics, for the #245 regression only: +# -f → --file-system (NOT BSD's "format"); handed a bogus %m operand it prints a +# multi-line fs block for the real file and exits nonzero. +# -c → format string ($2=%Y = mtime); prints a bare epoch integer, exit 0. +case "$1" in + -f) printf 'File: "%s"\nID: 0 Namelen: 255 Type: apfs\nBlock size: 4096\n' "${3:-$2}"; exit 1 ;; + -c) date +%s; exit 0 ;; + *) exit 1 ;; +esac +SHIM +chmod +x "$SHIMDIR/stat" +( export PATH="$SHIMDIR:$PATH"; bash "$HELPER" acquire --repo "$R" --id A --pid "$$" ) >/dev/null 2>&1 +assert_exit "f11 GNU-stat shim: fresh empty lock still held (BSD-first file_mtime regression #245)" 3 $? +rm -rf "$R" "$SHIMDIR" + print_summary