From a7d64c8225e3cf3647aa8e7e5b976792887d5674 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Fri, 21 Aug 2026 09:56:12 +0000 Subject: [PATCH] test(dirty-content-fingerprint): add dev-loop distinguishability regression Adds src/tools/test-dirty-fingerprint-distinguishability.sh, driving a real xce-build.sh scheduler cycle through the 4 required --version fingerprint states (clean/dirty-A/dirty-B-differs/reverted-A-matches). Uses a genuine isolated git worktree (git worktree add --detach at a unique mktemp -u path) to sidestep this actively-shared repo's ambient concurrent-editing noise -- all file edits confined strictly inside the isolated worktree, cleaned up via a trap registered before creation so it safely no-ops if creation never happened. Verified independently by review across two full live runs, including a deliberately sabotaged copy forcing edit-B to reproduce edit-A's content: the sabotaged run correctly failed with a clear message and exit 1, proving genuine discriminating power. git worktree list and the real repo's working tree confirmed byte-identical to baseline before and after every run. Co-Authored-By: Claude Sonnet 5 --- .../specs/dirty-content-fingerprint/tasks.md | 7 +- ...st-dirty-fingerprint-distinguishability.sh | 481 ++++++++++++++++++ 2 files changed, 485 insertions(+), 3 deletions(-) create mode 100755 src/tools/test-dirty-fingerprint-distinguishability.sh diff --git a/.kiro/specs/dirty-content-fingerprint/tasks.md b/.kiro/specs/dirty-content-fingerprint/tasks.md index 3886d451..08a5b4e8 100644 --- a/.kiro/specs/dirty-content-fingerprint/tasks.md +++ b/.kiro/specs/dirty-content-fingerprint/tasks.md @@ -94,9 +94,10 @@ - _Depends: 5.1_ - [ ] 6. Integration: cross-toolchain consistency and regression verification -- [ ] 6.1 Dev-loop distinguishability and clean-tree regression test - - New standalone script (following the `build-time-versioning` feature's `src/tools/test-*.sh` convention) drives a real `xce-build.sh scheduler` build cycle: build on a clean tree and confirm `--version` shows the commit id alone (no `-dirty` suffix, no fingerprint suffix); edit a tracked file, rebuild, confirm a fingerprint suffix appears and `-dirty` does not; edit again with different content, rebuild, confirm the fingerprint differs from the previous one; revert to the first edit's exact content, rebuild, confirm the fingerprint matches the first capture exactly - - Observable: the script exits non-zero and reports clearly if any of the four expected states (clean/no-suffix-at-all, dirty-A, dirty-B different from A, reverted-to-A matching A) does not hold, or if a literal `-dirty` substring ever reappears anywhere in `--version` output +- [x] 6.1 Dev-loop distinguishability and clean-tree regression test + - New script `src/tools/test-dirty-fingerprint-distinguishability.sh` uses a genuine isolated `git worktree add --detach` (unique `mktemp -u` path, `trap cleanup EXIT` registered before creation, safe no-op if creation never happened, `rm -rf`+`prune` fallback) to sidestep this actively-shared repo's ambient concurrent-editing noise -- all file edits confined strictly inside the isolated worktree, the real repo's own working tree never touched + - Drives a real `xce-build.sh scheduler` cycle through the 4 required states via genuine builds; fails loudly with a clear message on any mismatch + - Observable confirmed live (independently re-verified by review across two full live runs, including a deliberately sabotaged copy forcing edit-B to reproduce edit-A's content): clean/no-suffix, dirty-A, dirty-B-differs-from-A, reverted-to-A-matches-A all held with real fingerprints; sabotaged run correctly failed with a clear message and exit 1, proving genuine (not just synthetic) discriminating power; `git worktree list` and the real repo's working tree confirmed byte-identical to baseline before and after every run, both by implementer and reviewer - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1_ - _Depends: 2.1, 3.2_ diff --git a/src/tools/test-dirty-fingerprint-distinguishability.sh b/src/tools/test-dirty-fingerprint-distinguishability.sh new file mode 100755 index 00000000..a62fff42 --- /dev/null +++ b/src/tools/test-dirty-fingerprint-distinguishability.sh @@ -0,0 +1,481 @@ +#!/usr/bin/env bash +# test-dirty-fingerprint-distinguishability.sh — dirty-content-fingerprint +# task 6.1 (.kiro/specs/dirty-content-fingerprint/tasks.md, Requirements +# 1.1, 1.2, 1.3, 1.4, 2.1) +# +# MOTIVATING GAP this guards against: the already-shipped build-time- +# versioning feature's plain "-dirty" suffix cannot tell apart two builds +# made from the same commit but with DIFFERENT uncommitted content -- edit +# a file, rebuild, edit further, rebuild again: both binaries' --version +# output showed the exact same "-dirty" string. The +# dirty-content-fingerprint feature (tasks 1-5.2, already implemented and +# committed) replaces that boolean marker with a content-derived hash for +# --version specifically. THIS script is the end-to-end regression proof, +# via a REAL `xce-build.sh scheduler` build cycle, that the four states +# Requirement 1/2 require actually hold: clean tree -> commit id alone; +# edit A -> a fingerprint suffix (never "-dirty"); edit B (different +# content) -> a DIFFERENT fingerprint; revert to A's exact content -> the +# SAME fingerprint as A's original capture. +# +# ISOLATION FROM AMBIENT SESSION NOISE (read before changing this script): +# this repo is routinely under continuous, unrelated concurrent editing in +# this environment (other agent sessions, OpenWolf hooks checkpointing +# .wolf/buglog.json, etc.) -- `git status --porcelain` on the shared +# working tree this script lives in can be, and usually is, already dirty +# for reasons that have nothing to do with this test. A "clean tree" state +# (Requirement 2.1) can therefore not be reliably observed by pointing +# xce-build.sh at the ambient checkout. Instead, this script creates its +# own throwaway, fully-isolated `git worktree` checked out at the CURRENT +# HEAD commit (a linked worktree, `git worktree add --detach`) before doing +# anything else. That worktree starts genuinely clean by construction (a +# fresh checkout has zero uncommitted changes), and nothing outside this +# script's own controlled edits (to one file, inside that private +# worktree) can dirty it during the run -- concurrent activity in the +# original shared worktree is a completely separate directory tree and +# cannot touch it. This also sidesteps a subtler hazard: xce-build.sh +# resolves its own SRC_DIR from `$0`'s location and `cd`s there itself +# (`src/tools/xce-build.sh` line ~75-78) before running any `git` +# command -- invoking the ISOLATED worktree's own copy of the script (not +# the shared tree's copy from a different cwd) is what makes every git +# command inside it (rev-parse/status/diff/ls-files) scope to the isolated +# worktree automatically, with no extra `-C`/`cd` bookkeeping needed here. +# +# The isolated worktree is destroyed (git worktree remove --force) on +# exit unless --keep-worktree is given. This script's own edits never +# touch the shared working tree at all (they land only inside the +# throwaway worktree), so there is nothing to restore there -- verified +# explicitly at the end anyway (git status --porcelain on this repo's own +# README.md must be empty). +# +# Build path / directory-reuse note: all four builds reuse the SAME build +# directory inside the isolated worktree (build 1 is a normal fresh +# `meson setup`; builds 2-4 use xce-build.sh's own `--incremental` flag, +# i.e. `meson setup --reconfigure`, to avoid four full `rm -rf` + fresh +# `meson setup` cycles). This is safe here specifically because every +# build in this sequence targets the SAME single component (`scheduler`) +# in the SAME dir -- none of bug-1565's "another component's binary +# silently deleted" risk applies. Reusing one dir also matters for +# correctness, not just speed: meson auto-writes a self-ignoring +# `.gitignore` (`*`) into a build dir the moment `meson setup` first +# creates it, which is what keeps that build dir's own artifacts out of +# `git status --porcelain`/`git ls-files --others` -- and therefore out of +# xce-build.sh's own dirty_fingerprint computation -- for every build +# after the first. `debug` profile is used throughout: it is build-only +# (xce-build.sh's own RUN_TESTS=0 for `debug`), so no bundled `meson test` +# run can interfere with or slow down this script (unlike the `asan` +# profile the sibling task-6.1 script from build-time-versioning had to +# work around). +# +# Usage: +# bash src/tools/test-dirty-fingerprint-distinguishability.sh [--keep-worktree] +# +# Exit code is the sole pass/fail signal (0 = pass, non-zero = a real +# regression or a demonstration inconsistency). Four real build steps +# happen (one full, three incremental reconfigure+recompile) -- budget +# several minutes of wall-clock time. +# +# Why this file lives here: same convention as build-time-versioning's +# test-build-version-regression.sh / test-version-startup-consistency.sh / +# test-xceadmin-build-install-version.sh -- a standalone, CLI-runnable, +# real-build regression check for a cross-cutting concern, deliberately +# NOT a meson test() (real builds are far too expensive for every `meson +# test` run). +# +# Revalidation triggers: +# - VERSION_PREFIX below must match the literal prefix passed to +# xce_compose_version_ex() in scheduler/src/main.c (task 3.2). +# - The literal "-dirty" suffix text (XCE_DIRTY_SUFFIX in +# src/lib/utils.c) must stay "-dirty" for the substring check below to +# mean what it says. +set -euo pipefail + +# ── paths ─────────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORKTREE="$(cd "$SCRIPT_DIR/../.." && pwd)" + +VERSION_PREFIX="xce v0.0.0.1-" +BUILD_DIR_NAME="dirtyfp.debug" +README_REL="README.md" +MARKER_A="" +MARKER_B="" + +KEEP_WORKTREE=0 +for arg in "$@"; do + case "$arg" in + --keep-worktree) KEEP_WORKTREE=1 ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: $0 [--keep-worktree]" >&2 + exit 2 + ;; + esac +done + +# ── colour / pass-fail helpers (matches test-build-version-regression.sh / +# test-version-startup-consistency.sh) ─────────────────────────────────────── +green() { printf '\033[32m%s\033[0m\n' "$*"; } +red() { printf '\033[31m%s\033[0m\n' "$*"; } +yellow() { printf '\033[33m%s\033[0m\n' "$*"; } + +trap 'red "FATAL: command failed (line $LINENO). See output above."' ERR + +ISO_DIR="" + +cleanup() { + if [ "$KEEP_WORKTREE" = 1 ]; then + if [ -n "$ISO_DIR" ] && [ -d "$ISO_DIR" ]; then + yellow "--keep-worktree given: leaving isolated worktree in place at $ISO_DIR" + fi + return 0 + fi + if [ -n "$ISO_DIR" ] && [ -d "$ISO_DIR" ]; then + git -C "$WORKTREE" worktree remove --force "$ISO_DIR" 2>/dev/null || { + rm -rf "$ISO_DIR" + git -C "$WORKTREE" worktree prune 2>/dev/null || true + } + fi +} +trap cleanup EXIT + +# ── pure extraction helpers (no printing -- safe to call via command +# substitution; mirrors test-build-version-regression.sh's +# extract_commit_id) ───────────────────────────────────────────────────────── + +# extract_commit_id RAW -> prints "" (may itself contain a +# "-" suffix, or not). Returns 1 if VERSION_PREFIX is absent. +extract_commit_id() { + local raw="$1" rest + rest="${raw#*"$VERSION_PREFIX"}" + if [ "$rest" = "$raw" ]; then + return 1 + fi + printf '%s' "${rest%% built *}" +} + +# extract_fingerprint COMMIT_FIELD BASE_COMMIT -> prints the fingerprint +# suffix (empty string if COMMIT_FIELD is exactly BASE_COMMIT, i.e. no +# suffix at all -- the clean-tree shape). Returns 1 if COMMIT_FIELD is +# neither exactly BASE_COMMIT nor "BASE_COMMIT-" -- a stronger +# failure than "wrong fingerprint" (the base commit id itself is wrong). +extract_fingerprint() { + local commit_field="$1" base="$2" prefixed + if [ "$commit_field" = "$base" ]; then + printf '' + return 0 + fi + prefixed="${base}-" + if [ "${commit_field#"$prefixed"}" != "$commit_field" ]; then + printf '%s' "${commit_field#"$prefixed"}" + return 0 + fi + return 1 +} + +# has_dirty_substring RAW -> return 0 (true) iff the literal substring +# "-dirty" appears anywhere in RAW. +has_dirty_substring() { + case "$1" in + *-dirty*) return 0 ;; + *) return 1 ;; + esac +} + +# ── the single validating function every real check AND the demonstration +# below both exercise (mirrors check_result/check_consistency in the +# build-time-versioning sibling scripts) ───────────────────────────────────── +# +# check_capture LABEL RAW BASE_COMMIT MODE(clean|dirty) +# On success: prints a PASS line, sets global FP_OUT to the extracted +# fingerprint (empty for "clean"), returns 0. +# On failure: prints a FAIL line with a clear reason, returns 1. FP_OUT is +# reset to "" first so a stale value can never leak past a failure. +FP_OUT="" +check_capture() { + local label="$1" raw="$2" base="$3" mode="$4" + local commit_field fp + FP_OUT="" + + if has_dirty_substring "$raw"; then + red "FAIL [$label]: raw --version output contains a literal '-dirty' substring -- this must never reappear once a fingerprint is in play. Raw: [$raw]" + return 1 + fi + + if ! commit_field="$(extract_commit_id "$raw")"; then + red "FAIL [$label]: --version output did not contain the expected prefix '$VERSION_PREFIX'. Raw: [$raw]" + return 1 + fi + + if ! fp="$(extract_fingerprint "$commit_field" "$base")"; then + red "FAIL [$label]: commit field '$commit_field' does not match expected base commit '$base', with or without a fingerprint suffix. Raw: [$raw]" + return 1 + fi + + if [ "$mode" = "clean" ] && [ -n "$fp" ]; then + red "FAIL [$label]: expected the bare commit id with NO suffix (clean tree, Requirement 2.1), but got fingerprint suffix '$fp'. Raw: [$raw]" + return 1 + fi + if [ "$mode" = "dirty" ] && [ -z "$fp" ]; then + red "FAIL [$label]: expected a fingerprint suffix (dirty tree, Requirement 1.1), but --version showed the bare commit id with no suffix at all. Raw: [$raw]" + return 1 + fi + + FP_OUT="$fp" + green "PASS [$label]: commit field='$commit_field' fingerprint='${fp:-}'" + return 0 +} + +# assert_fp_differs / assert_fp_matches -- the discriminating comparisons +# Requirement 1.2/1.3 require. Print their own PASS/FAIL, return 0/1. +assert_fp_differs() { + local label="$1" fp_a="$2" fp_b="$3" + if [ "$fp_a" = "$fp_b" ]; then + red "FAIL [$label]: fingerprints are IDENTICAL ('$fp_a') but were expected to differ (different uncommitted content, Requirement 1.2)." + return 1 + fi + green "PASS [$label]: fingerprints differ ('$fp_a' vs '$fp_b'), as required for different content." + return 0 +} +assert_fp_matches() { + local label="$1" fp_a="$2" fp_b="$3" + if [ "$fp_a" != "$fp_b" ]; then + red "FAIL [$label]: fingerprints DIFFER ('$fp_a' vs '$fp_b') but were expected to match exactly (identical uncommitted content, Requirement 1.3)." + return 1 + fi + green "PASS [$label]: fingerprints match exactly ('$fp_a'), as required for identical content." + return 0 +} + +# Proves check_capture/assert_fp_differs/assert_fp_matches -- the EXACT +# functions the real flow below uses -- would themselves have caught a +# regression: a wrong base commit, a resurrected literal "-dirty" +# substring, a missing/unexpected suffix for the given mode, and both +# comparison directions (identical-when-should-differ, +# different-when-should-match), plus a sanity check that genuine real +# values still pass. +demonstrate_detection_capability() { + local base="$1" real_fp_a="$2" real_fp_b="$3" + + yellow "=== DEMONSTRATION: proving the comparison logic catches every required failure mode ===" + + yellow "--- (a) wrong base commit id (prefix/format fine, base mismatched) ---" + if check_capture "DEMO-wrong-base" "${VERSION_PREFIX}deadbeef built 2026-01-01T00:00:00Z" "notdeadbeef" "clean"; then + red "DEMONSTRATION FAILED: incorrectly PASSED against a mismatched base commit." + exit 1 + fi + green "Confirmed: a mismatched base commit id is correctly rejected." + + yellow "--- (b) a literal '-dirty' substring resurfacing despite a valid prefix/base ---" + if check_capture "DEMO-dirty-substring" "${VERSION_PREFIX}${base}-dirty built 2026-01-01T00:00:00Z" "$base" "clean"; then + red "DEMONSTRATION FAILED: incorrectly PASSED output containing a literal '-dirty' substring." + exit 1 + fi + green "Confirmed: a resurfaced literal '-dirty' substring is correctly rejected." + + yellow "--- (c) dirty mode expects a fingerprint suffix; synthetic output has none ---" + if check_capture "DEMO-dirty-no-suffix" "${VERSION_PREFIX}${base} built 2026-01-01T00:00:00Z" "$base" "dirty"; then + red "DEMONSTRATION FAILED: incorrectly PASSED a dirty-mode capture with no fingerprint suffix at all." + exit 1 + fi + green "Confirmed: a missing fingerprint suffix where one is required is correctly rejected." + + yellow "--- (d) clean mode forbids a fingerprint suffix; synthetic output has one ---" + if check_capture "DEMO-clean-has-suffix" "${VERSION_PREFIX}${base}-abcd1234 built 2026-01-01T00:00:00Z" "$base" "clean"; then + red "DEMONSTRATION FAILED: incorrectly PASSED a clean-mode capture that carries a fingerprint suffix." + exit 1 + fi + green "Confirmed: an unexpected fingerprint suffix on a clean-mode capture is correctly rejected." + + yellow "--- (e) sanity: genuinely well-formed captures must still PASS ---" + if ! check_capture "DEMO-sanity-clean" "${VERSION_PREFIX}${base} built 2026-01-01T00:00:00Z" "$base" "clean"; then + red "DEMONSTRATION FAILED: incorrectly FAILED a genuinely clean, well-formed capture." + exit 1 + fi + if ! check_capture "DEMO-sanity-dirty" "${VERSION_PREFIX}${base}-${real_fp_a} built 2026-01-01T00:00:00Z" "$base" "dirty"; then + red "DEMONSTRATION FAILED: incorrectly FAILED a genuinely dirty, well-formed capture." + exit 1 + fi + green "Confirmed: genuinely well-formed captures pass in both modes." + + yellow "--- (f) differs-check / matches-check discriminating power ---" + if assert_fp_differs "DEMO-differs-on-identical" "$real_fp_a" "$real_fp_a"; then + red "DEMONSTRATION FAILED: differs-check incorrectly PASSED on two identical fingerprint strings." + exit 1 + fi + green "Confirmed: the differs-check correctly rejects two identical fingerprints." + + if assert_fp_matches "DEMO-matches-on-different" "$real_fp_a" "$real_fp_b"; then + red "DEMONSTRATION FAILED: matches-check incorrectly PASSED on two different fingerprint strings." + exit 1 + fi + green "Confirmed: the matches-check correctly rejects two different fingerprints." + + if ! assert_fp_differs "DEMO-sanity-differs" "$real_fp_a" "$real_fp_b"; then + red "DEMONSTRATION FAILED: differs-check incorrectly FAILED on genuinely different real fingerprints ('$real_fp_a' vs '$real_fp_b')." + exit 1 + fi + if ! assert_fp_matches "DEMO-sanity-matches" "$real_fp_a" "$real_fp_a"; then + red "DEMONSTRATION FAILED: matches-check incorrectly FAILED on two copies of the same real fingerprint." + exit 1 + fi + + green "=== DEMONSTRATION complete: wrong base commit / resurfaced '-dirty' / missing or unexpected suffix / identical-when-should-differ / different-when-should-match are ALL rejected; genuine matches are accepted ===" +} + +# ── build helper (mirrors run_build() in test-build-version-regression.sh; +# debug profile never runs a bundled test suite, but the same defensive +# non-abort-on-nonzero-exit + binary-existence-check pattern is kept for +# consistency and because a genuine compile failure must still be caught) ── +run_build() { + local iso_src="$1" incremental_flag="$2" rc=0 + if [ "$incremental_flag" = 1 ]; then + yellow "=== nix develop $iso_src --command bash $iso_src/tools/xce-build.sh scheduler debug $BUILD_DIR_NAME --incremental ===" + nix develop "$iso_src" --command bash "$iso_src/tools/xce-build.sh" scheduler debug "$BUILD_DIR_NAME" --incremental || rc=$? + else + yellow "=== nix develop $iso_src --command bash $iso_src/tools/xce-build.sh scheduler debug $BUILD_DIR_NAME ===" + nix develop "$iso_src" --command bash "$iso_src/tools/xce-build.sh" scheduler debug "$BUILD_DIR_NAME" || rc=$? + fi + if [ "$rc" != 0 ]; then + yellow "WARNING: xce-build.sh exited non-zero (rc=$rc). Proceeding to check whether the binary was still produced -- the binary-existence check right after this call is the authoritative compile-failure signal." + fi + return 0 +} + +capture_version() { + # NOTE: iso_src and binary are deliberately two separate `local` + # statements, not one combined `local a=... b="$a/..."` -- bash expands + # every word of a single `local` command before any of that same + # command's assignments take effect, so a same-statement cross- + # reference like that would silently see the OLD (unset) value of + # iso_src under `set -u`. Found live while first running this script. + local iso_src="$1" + local binary="$iso_src/$BUILD_DIR_NAME/scheduler/xce_scheduler" + if [ ! -x "$binary" ]; then + red "FATAL: expected binary not found/executable at $binary after a successful-looking build." + exit 1 + fi + "$binary" --version 2>&1 +} + +# ══════════════════════════════════════════════════════════════════════════ +# Set up the isolated worktree +# ══════════════════════════════════════════════════════════════════════════ +ISO_DIR="$(mktemp -u "${TMPDIR:-/tmp}/xce-dirty-fp-worktree.XXXXXX")" +yellow "=== Creating isolated git worktree at HEAD (detached), immune to ambient concurrent edits in $WORKTREE: $ISO_DIR ===" +git -C "$WORKTREE" worktree add --detach "$ISO_DIR" HEAD >/dev/null +ISO_SRC="$ISO_DIR/src" + +if [ ! -f "$ISO_SRC/tools/xce-build.sh" ]; then + red "FATAL: isolated worktree does not contain $ISO_SRC/tools/xce-build.sh -- unexpected checkout layout." + exit 1 +fi + +untracked_at_start="$(git -C "$ISO_DIR" status --porcelain)" +if [ -n "$untracked_at_start" ]; then + red "FATAL: freshly-created isolated worktree is not clean at creation time -- this should be impossible for a fresh 'git worktree add'. Status:" + printf '%s\n' "$untracked_at_start" >&2 + exit 1 +fi +base_commit="$(git -C "$ISO_DIR" rev-parse --short HEAD)" +yellow "Isolated worktree base commit (clean, by construction): $base_commit" + +readme_path="$ISO_DIR/$README_REL" +if [ ! -f "$readme_path" ]; then + red "FATAL: expected $readme_path to exist -- pick a different safe edit target if this repo's layout changed." + exit 1 +fi +readme_original="$(cat "$readme_path")" + +overall_pass=1 + +# ══════════════════════════════════════════════════════════════════════════ +# State 1: clean tree -- commit id alone, no suffix at all (Requirement 2.1) +# ══════════════════════════════════════════════════════════════════════════ +run_build "$ISO_SRC" 0 +raw_clean="$(capture_version "$ISO_SRC")" +yellow "[clean] --version raw output: [$raw_clean]" +check_capture "clean" "$raw_clean" "$base_commit" "clean" || overall_pass=0 + +# ══════════════════════════════════════════════════════════════════════════ +# State 2: edit A -- fingerprint suffix present, never "-dirty" (Req 1.1) +# ══════════════════════════════════════════════════════════════════════════ +printf '\n%s\n' "$MARKER_A" >> "$readme_path" +run_build "$ISO_SRC" 1 +raw_a="$(capture_version "$ISO_SRC")" +yellow "[dirty-A] --version raw output: [$raw_a]" +fp_a="" +if check_capture "dirty-A" "$raw_a" "$base_commit" "dirty"; then + fp_a="$FP_OUT" +else + overall_pass=0 +fi + +# ══════════════════════════════════════════════════════════════════════════ +# State 3: edit B, DIFFERENT content -- a DIFFERENT fingerprint (Req 1.2) +# ══════════════════════════════════════════════════════════════════════════ +git -C "$ISO_DIR" checkout -- "$README_REL" +printf '\n%s\n' "$MARKER_B" >> "$readme_path" +run_build "$ISO_SRC" 1 +raw_b="$(capture_version "$ISO_SRC")" +yellow "[dirty-B] --version raw output: [$raw_b]" +fp_b="" +if check_capture "dirty-B" "$raw_b" "$base_commit" "dirty"; then + fp_b="$FP_OUT" +else + overall_pass=0 +fi +if [ -n "$fp_a" ] && [ -n "$fp_b" ]; then + assert_fp_differs "dirty-B-vs-A" "$fp_a" "$fp_b" || overall_pass=0 +fi + +# ══════════════════════════════════════════════════════════════════════════ +# State 4: revert to A's exact content -- fingerprint matches A exactly +# (Requirement 1.3) +# ══════════════════════════════════════════════════════════════════════════ +git -C "$ISO_DIR" checkout -- "$README_REL" +printf '\n%s\n' "$MARKER_A" >> "$readme_path" +run_build "$ISO_SRC" 1 +raw_revert="$(capture_version "$ISO_SRC")" +yellow "[reverted-to-A] --version raw output: [$raw_revert]" +fp_revert="" +if check_capture "reverted-to-A" "$raw_revert" "$base_commit" "dirty"; then + fp_revert="$FP_OUT" +else + overall_pass=0 +fi +if [ -n "$fp_a" ] && [ -n "$fp_revert" ]; then + assert_fp_matches "reverted-vs-A" "$fp_a" "$fp_revert" || overall_pass=0 +fi + +# ── restore the isolated worktree's README.md before the worktree is torn +# down (tidiness only -- the whole directory is about to be removed either +# way, but this keeps a --keep-worktree run's `git status` honest too) ────── +printf '%s' "$readme_original" > "$readme_path" + +# ══════════════════════════════════════════════════════════════════════════ +# Discrimination demonstration +# ══════════════════════════════════════════════════════════════════════════ +if [ -n "$fp_a" ] && [ -n "$fp_b" ]; then + demonstrate_detection_capability "$base_commit" "$fp_a" "$fp_b" +else + red "FATAL: could not obtain two real, distinct fingerprints from the actual build sequence above -- cannot run the demonstration. This itself means the real sequence already failed." + overall_pass=0 +fi + +# ── constraint check: the SHARED working tree's own README.md must be +# completely untouched by this run (all edits above happened only inside +# the throwaway isolated worktree) ────────────────────────────────────────── +shared_readme_diff="$(git -C "$WORKTREE" status --porcelain -- "$README_REL")" +if [ -n "$shared_readme_diff" ]; then + red "FAIL: the SHARED working tree's own $README_REL was modified by this run -- it must never be touched (all edits must land only in the isolated worktree). git status: [$shared_readme_diff]" + overall_pass=0 +else + green "PASS: shared working tree's own $README_REL is untouched (git status --porcelain empty)." +fi + +if [ "$overall_pass" = 1 ]; then + green "=== ALL CHECKS PASS: clean tree shows the commit id alone; dirty-A carries a fingerprint (never '-dirty'); dirty-B's fingerprint differs from A's; reverting to A's exact content reproduces A's fingerprint exactly ===" + exit 0 +else + red "=== REGRESSION DETECTED: see FAIL line(s) above ===" + exit 1 +fi