Skip to content

Commit 29d6523

Browse files
mikolalysenkoclaude
andcommitted
test(setup-matrix): verify check/remove behaviorally via install sequences
The previous round-trip inspected package.json (grepped for socket-patch) and flag state. Replace it with a behavioral verification driven by real (setup)·(package-manager install) cycles, for npm-family cases that run setup: install -> patch NOT applied (no hook yet) setup --check -> fails (not configured) setup --yes; setup --check -> passes (configured) reinstall -> patch applied (hook fires; the main actual_applied) setup --remove --yes; setup --check -> fails (reverted) reinstall -> patch NOT applied (hook gone) A clean `rm -rf node_modules` precedes each observation so the lifecycle hook acts on a pristine package. run-case.sh factors out do_install/verify_applied/ reset_modules; emit_result drops `remove_clean` and adds applied_before_setup, applied_after_remove, and check_before_setup_exit. The harness round_trip_failure asserts the patch bookends are not-applied and check goes false->true->false. Non-npm / no-setup cases keep the simple single-install flow unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8da8f38 commit 29d6523

2 files changed

Lines changed: 133 additions & 68 deletions

File tree

crates/socket-patch-cli/tests/setup_matrix_common/mod.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -330,22 +330,40 @@ fn run_cases(label: &str, cases: Vec<Case>) {
330330
);
331331
}
332332

333-
/// Validate the `setup --check` / `setup --remove` round-trip fields emitted by
334-
/// the driver. Returns a failure message if the inverse of setup did not hold:
335-
/// after configuring, `--check` must pass (0); `--remove` must succeed (0) and
336-
/// strip socket-patch from package.json; and `--check` must then fail again
337-
/// (non-zero). Returns `None` on success.
333+
/// Validate the behavioral `(setup)·(install)` round-trip emitted by the driver.
334+
/// Verifies — through real install cycles, not by reading package.json — that:
335+
///
336+
/// 1. `setup --check` fails before setup, passes after setup, fails after
337+
/// `setup --remove` (and remove itself succeeds);
338+
/// 2. the patch is NOT applied before setup and NOT applied after remove
339+
/// (the after-setup application is covered separately by the main
340+
/// `actual_applied == expect_applied` assertion).
341+
///
342+
/// Returns a failure message describing any violation, or `None` on success.
338343
fn round_trip_failure(case: &Case, res: &RunResult) -> Option<String> {
339344
let parsed = res.parsed.as_ref()?;
340345
let int = |k: &str| parsed.get(k).and_then(|v| v.as_i64());
341346
let boolean = |k: &str| parsed.get(k).and_then(|v| v.as_bool());
342347

348+
let mut problems = Vec::new();
349+
350+
// (2) patch application bookends — only ever true while the hook is wired.
351+
if boolean("applied_before_setup") == Some(true) {
352+
problems.push("patch applied BEFORE setup (no hook should be configured yet)".to_string());
353+
}
354+
if boolean("applied_after_remove") == Some(true) {
355+
problems.push("patch still applied AFTER remove (hook should be gone)".to_string());
356+
}
357+
358+
// (1) `setup --check` tracks the configured state: false → true → false.
359+
let check_before = int("check_before_setup_exit");
343360
let check_setup = int("check_after_setup_exit");
344361
let remove = int("remove_exit");
345362
let check_remove = int("check_after_remove_exit");
346-
let clean = boolean("remove_clean");
347363

348-
let mut problems = Vec::new();
364+
if check_before == Some(0) {
365+
problems.push("check-before-setup exit=0 (want non-zero; not configured yet)".to_string());
366+
}
349367
if check_setup != Some(0) {
350368
problems.push(format!("check-after-setup exit={check_setup:?} (want 0)"));
351369
}
@@ -355,15 +373,12 @@ fn round_trip_failure(case: &Case, res: &RunResult) -> Option<String> {
355373
if check_remove == Some(0) {
356374
problems.push("check-after-remove exit=0 (want non-zero; hook still present)".to_string());
357375
}
358-
if clean == Some(false) {
359-
problems.push("socket-patch still present in package.json after remove".to_string());
360-
}
361376

362377
if problems.is_empty() {
363378
return None;
364379
}
365380
Some(format!(
366-
" - {}: check/remove round-trip failed [{}]\n{}",
381+
" - {}: setup/install behavioral round-trip failed [{}]\n{}",
367382
case.id,
368383
problems.join("; "),
369384
indent(&res.raw)

tests/setup_matrix/run-case.sh

Lines changed: 107 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,14 @@ log() { printf '[setup-matrix:%s] %s\n' "$SM_ID" "$*"; }
7777
json_str() { printf '%s' "$1" | tr -d '\r' | tr '\n' ' ' | sed 's/\\/\\\\/g; s/"/\\"/g'; }
7878
emit_result() {
7979
local actual="$1" primary_present="$2" setup_exit="$3" install_exit="$4" target="$5" status="$6"
80-
printf '{"id":"%s","ecosystem":"%s","pm":"%s","scenario":"%s","patchset":"%s","run_setup":%s,"expect_applied":%s,"actual_applied":%s,"primary_marker_present":%s,"setup_exit":%s,"install_exit":%s,"check_after_setup_exit":%s,"remove_exit":%s,"check_after_remove_exit":%s,"remove_clean":%s,"target":"%s","status":"%s","notes":"%s"}\n' \
80+
printf '{"id":"%s","ecosystem":"%s","pm":"%s","scenario":"%s","patchset":"%s","run_setup":%s,"expect_applied":%s,"actual_applied":%s,"applied_before_setup":%s,"applied_after_remove":%s,"primary_marker_present":%s,"setup_exit":%s,"install_exit":%s,"check_before_setup_exit":%s,"check_after_setup_exit":%s,"remove_exit":%s,"check_after_remove_exit":%s,"target":"%s","status":"%s","notes":"%s"}\n' \
8181
"$(json_str "$SM_ID")" "$(json_str "$SM_ECOSYSTEM")" "$(json_str "$SM_PM")" \
8282
"$(json_str "$SM_SCENARIO")" "$(json_str "$SM_PATCHSET")" \
8383
"$([ "$SM_RUN_SETUP" = 1 ] && echo true || echo false)" \
8484
"$([ "$SM_EXPECT_APPLIED" = 1 ] && echo true || echo false)" \
85-
"$actual" "$primary_present" "$setup_exit" "$install_exit" \
86-
"${CHECK_AFTER_SETUP_EXIT:-null}" "${REMOVE_EXIT:-null}" "${CHECK_AFTER_REMOVE_EXIT:-null}" "${REMOVE_CLEAN:-null}" \
85+
"$actual" "${APPLIED_BEFORE_SETUP:-null}" "${APPLIED_AFTER_REMOVE:-null}" "$primary_present" \
86+
"$setup_exit" "$install_exit" \
87+
"${CHECK_BEFORE_SETUP_EXIT:-null}" "${CHECK_AFTER_SETUP_EXIT:-null}" "${REMOVE_EXIT:-null}" "${CHECK_AFTER_REMOVE_EXIT:-null}" \
8788
"$(json_str "$target")" "$(json_str "$status")" "$(json_str "$NOTES")" >&3
8889
}
8990

@@ -449,6 +450,49 @@ resolve_targets() {
449450
esac
450451
}
451452

453+
# --- native install dispatch (layout-aware) --------------------------
454+
do_install() {
455+
case "$SM_LAYOUT" in
456+
workspace) run_install_workspace ;;
457+
monorepo) run_install_monorepo ;;
458+
*) run_install ;;
459+
esac
460+
}
461+
462+
# Wipe installed modules so the NEXT install re-fetches a pristine copy and
463+
# re-fires the lifecycle hook. This is what lets us observe the patch-apply
464+
# BEHAVIOR (marker present/absent on a freshly installed file) at each stage
465+
# of the (setup)·(install) sequence, rather than inspecting package.json.
466+
reset_modules() {
467+
rm -rf node_modules packages/*/node_modules 2>/dev/null || true
468+
}
469+
470+
# Resolve every on-disk copy of the patched file and decide whether the patch
471+
# was applied (marker present). Sets APPLIED / PRIMARY_PRESENT / TARGET.
472+
verify_applied() {
473+
local check_marker="$SM_MARKER"
474+
[ "$SM_PATCHSET" = alt ] && check_marker="$SM_ALT_MARKER"
475+
APPLIED=false
476+
PRIMARY_PRESENT=null
477+
TARGET=""
478+
local n_found=0 cand
479+
while IFS= read -r cand; do
480+
[ -n "$cand" ] && [ -f "$cand" ] || continue
481+
n_found=$((n_found + 1))
482+
[ -z "$TARGET" ] && TARGET="$cand"
483+
if grep -q "$check_marker" "$cand" 2>/dev/null; then APPLIED=true; TARGET="$cand"; fi
484+
if grep -q "$SM_MARKER" "$cand" 2>/dev/null; then PRIMARY_PRESENT=true; fi
485+
done < <(resolve_targets)
486+
[ "$PRIMARY_PRESENT" = null ] && [ "$n_found" -gt 0 ] && PRIMARY_PRESENT=false
487+
log "verify: marker '$check_marker' present=$APPLIED (candidates=$n_found, target=${TARGET:-<none>})"
488+
}
489+
490+
# npm-family is the surface `setup` actually configures today — the only place
491+
# the behavioral check/remove round-trip is expected to do real work.
492+
is_npm_family() {
493+
[[ "$SM_PM" =~ ^(npm|yarn|pnpm|bun)$ ]] || [ "$SM_LAYOUT" = monorepo ]
494+
}
495+
452496
# ============================ main ====================================
453497
log "binary: $SP_BIN ($("$SP_BIN" --version 2>/dev/null || echo '??')) layout=$SM_LAYOUT"
454498

@@ -494,76 +538,82 @@ export SOCKET_TELEMETRY_DISABLED=1 SOCKET_EXPERIMENTAL_MAVEN=1 SOCKET_EXPERIMENT
494538
# make every member apply target the root manifest and fail with "no
495539
# packages found on disk" mid-install, breaking `npm install`.
496540

497-
# 1. setup (configures hooks; no-op where there is no package.json)
541+
# 1-3. Configure + install + verify.
542+
#
543+
# For npm-family cases that run setup we exercise the FULL behavioral sequence
544+
# — (install)·(setup)·(install)·(remove)·(install) — observing both the patch
545+
# marker and `setup --check` at each stage. A clean reinstall precedes every
546+
# observation so the lifecycle hook acts on a pristine package. This verifies
547+
# behavior end-to-end rather than reading package.json:
548+
# * patch: NOT applied before setup → applied after setup → NOT applied after remove
549+
# * check: fails before setup → passes after setup → fails after remove
550+
#
551+
# Every other case (run_setup=0, or non-npm-family ecosystems) keeps the simple
552+
# single-install flow, preserving the existing aspirational expect_applied
553+
# classification untouched.
498554
SETUP_EXIT="null"
555+
CHECK_BEFORE_SETUP_EXIT="null"
499556
CHECK_AFTER_SETUP_EXIT="null"
500-
if [ "$SM_RUN_SETUP" = 1 ]; then
557+
REMOVE_EXIT="null"
558+
CHECK_AFTER_REMOVE_EXIT="null"
559+
APPLIED_BEFORE_SETUP=null
560+
APPLIED_AFTER_REMOVE=null
561+
INSTALL_EXIT="null"
562+
563+
if is_npm_family && [ "$SM_RUN_SETUP" = 1 ]; then
564+
# (1) BEFORE setup: no hook configured → install must NOT apply the patch.
565+
log "[before-setup] install for pm=$SM_PM (layout=$SM_LAYOUT)"
566+
do_install; log "[before-setup] install exit=$?"
567+
verify_applied; APPLIED_BEFORE_SETUP="$APPLIED"
568+
569+
# (2) check must report "needs configuration" (non-zero) before setup.
570+
"$SP_BIN" setup --check --json; CHECK_BEFORE_SETUP_EXIT=$?
571+
log "check-before-setup exit=$CHECK_BEFORE_SETUP_EXIT"
572+
573+
# (3) setup, then check must report "configured" (zero).
501574
log "running: socket-patch setup --yes"
502575
"$SP_BIN" setup --yes --json; SETUP_EXIT=$?
503576
log "setup exit=$SETUP_EXIT"
504577
[ -f package.json ] && { log "package.json scripts after setup:"; grep -A6 '"scripts"' package.json || true; }
505-
506-
# Read-only verification: a project we just configured must pass --check
507-
# (exit 0). Recorded for the harness; does not touch disk.
508-
log "running: socket-patch setup --check (after setup)"
509578
"$SP_BIN" setup --check --json; CHECK_AFTER_SETUP_EXIT=$?
510579
log "check-after-setup exit=$CHECK_AFTER_SETUP_EXIT"
511-
fi
512580

513-
# 2. native install (this is where a configured hook fires)
514-
log "running install for pm=$SM_PM (layout=$SM_LAYOUT)"
515-
case "$SM_LAYOUT" in
516-
workspace) run_install_workspace ;;
517-
monorepo) run_install_monorepo ;;
518-
*) run_install ;;
519-
esac
520-
INSTALL_EXIT=$?
521-
log "install exit=$INSTALL_EXIT"
522-
523-
# 3. verify — applied if ANY discovered copy of the patched file carries
524-
# the expected marker (covers hoisting, the pnpm store, member dirs and
525-
# the shared venv in workspace/monorepo layouts).
526-
check_marker="$SM_MARKER"
527-
[ "$SM_PATCHSET" = alt ] && check_marker="$SM_ALT_MARKER"
528-
APPLIED=false
529-
PRIMARY_PRESENT=null
530-
TARGET=""
531-
n_found=0
532-
while IFS= read -r cand; do
533-
[ -n "$cand" ] && [ -f "$cand" ] || continue
534-
n_found=$((n_found + 1))
535-
[ -z "$TARGET" ] && TARGET="$cand"
536-
if grep -q "$check_marker" "$cand" 2>/dev/null; then APPLIED=true; TARGET="$cand"; fi
537-
if grep -q "$SM_MARKER" "$cand" 2>/dev/null; then PRIMARY_PRESENT=true; fi
538-
done < <(resolve_targets)
539-
[ "$PRIMARY_PRESENT" = null ] && [ "$n_found" -gt 0 ] && PRIMARY_PRESENT=false
540-
note "candidate files found: $n_found"
541-
log "resolved target: ${TARGET:-<none>} (candidates=$n_found)"
542-
[ "$n_found" -eq 0 ] && note "target file not found"
543-
log "marker '$check_marker' present: $APPLIED"
544-
545-
# 4. remove round-trip — only meaningful where setup configured hooks.
546-
# Done LAST (after install + verify) so it cannot disturb the apply check.
547-
# Asserts the inverse of setup: --remove strips the hook, --check then fails,
548-
# and `socket-patch` no longer appears in the root package.json.
549-
REMOVE_EXIT="null"
550-
CHECK_AFTER_REMOVE_EXIT="null"
551-
REMOVE_CLEAN="null"
552-
if [ "$SM_RUN_SETUP" = 1 ] && [ -f package.json ]; then
581+
# (4) AFTER setup: clean reinstall → the hook fires → MAIN applied result.
582+
reset_modules
583+
log "[after-setup] install for pm=$SM_PM (layout=$SM_LAYOUT)"
584+
do_install; INSTALL_EXIT=$?
585+
log "[after-setup] install exit=$INSTALL_EXIT"
586+
verify_applied # sets the canonical APPLIED / PRIMARY_PRESENT / TARGET
587+
588+
# (5) remove, then check must report "needs configuration" (non-zero) again.
553589
log "running: socket-patch setup --remove --yes"
554590
"$SP_BIN" setup --remove --yes --json; REMOVE_EXIT=$?
555591
log "remove exit=$REMOVE_EXIT"
556-
log "package.json scripts after remove:"; grep -A6 '"scripts"' package.json || true
557-
558-
log "running: socket-patch setup --check (after remove)"
592+
[ -f package.json ] && { log "package.json scripts after remove:"; grep -A6 '"scripts"' package.json || true; }
559593
"$SP_BIN" setup --check --json; CHECK_AFTER_REMOVE_EXIT=$?
560594
log "check-after-remove exit=$CHECK_AFTER_REMOVE_EXIT"
561595

562-
if grep -q "socket-patch" package.json 2>/dev/null; then
563-
REMOVE_CLEAN=false; note "remove left socket-patch in root package.json"
564-
else
565-
REMOVE_CLEAN=true
596+
# (6) AFTER remove: clean reinstall → no hook → must NOT apply the patch.
597+
# Preserve the main (after-setup) result while re-probing the disk.
598+
_MAIN_APPLIED="$APPLIED"; _MAIN_PRIMARY="$PRIMARY_PRESENT"; _MAIN_TARGET="$TARGET"
599+
reset_modules
600+
log "[after-remove] install for pm=$SM_PM (layout=$SM_LAYOUT)"
601+
do_install; log "[after-remove] install exit=$?"
602+
verify_applied; APPLIED_AFTER_REMOVE="$APPLIED"
603+
APPLIED="$_MAIN_APPLIED"; PRIMARY_PRESENT="$_MAIN_PRIMARY"; TARGET="$_MAIN_TARGET"
604+
else
605+
# Simple flow: optional setup (no-op where there is no package.json), one
606+
# install, one verify.
607+
if [ "$SM_RUN_SETUP" = 1 ]; then
608+
log "running: socket-patch setup --yes"
609+
"$SP_BIN" setup --yes --json; SETUP_EXIT=$?
610+
log "setup exit=$SETUP_EXIT"
611+
[ -f package.json ] && { log "package.json scripts after setup:"; grep -A6 '"scripts"' package.json || true; }
566612
fi
613+
log "running install for pm=$SM_PM (layout=$SM_LAYOUT)"
614+
do_install; INSTALL_EXIT=$?
615+
log "install exit=$INSTALL_EXIT"
616+
verify_applied
567617
fi
568618

569619
# Driver-level status: did actual match the aspirational expectation?

0 commit comments

Comments
 (0)