fix: make local recovery fail closed

This commit is contained in:
2026-08-05 09:48:55 +02:00
parent 65aa52115f
commit df21046472
4 changed files with 577 additions and 115 deletions
+77 -1
View File
@@ -12,6 +12,7 @@ trap 'rm -f "$output" "$verifier_functions"; rm -rf "$negative_root"' EXIT HUP I
for fixture in \
"local installation guide contract" \
"source update fail-closed semantics" \
"Windows line-ending recovery guide contract" \
"Pi management guide contract" \
"local installation example rendered from path with spaces" \
@@ -20,7 +21,7 @@ for fixture in \
"canonical local base+override fixture" \
"canonical server base+override fixture" \
"relative secret-source fixture rejected" \
"CRLF recovery rewrites worktree bytes"; do
"CRLF recovery rewrites bytes and preserves mode-120000 symlinks"; do
grep -Fqx "$fixture passed" "$output" >/dev/null || {
echo "missing fixture verification: $fixture" >&2
cat "$output" >&2
@@ -66,6 +67,10 @@ expect_guide_rejected() {
local fixture_output="$fixture_root/output"
mkdir -p "$fixture_root/$(dirname "$relative_path")"
cp "$source_guide" "$fixture_root/$relative_path"
if [[ "$validator" == verify_windows_line_endings_guide ]]; then
mkdir -p "$fixture_root/scripts"
cp "$root/scripts/verify-line-endings.sh" "$fixture_root/scripts/verify-line-endings.sh"
fi
node - "$fixture_root/$relative_path" "$mutation" <<'NODE'
const fs = require("fs");
const [path, mutation] = process.argv.slice(2);
@@ -87,6 +92,36 @@ switch (mutation) {
case "raw-pi":
changed += "\n```sh\ndocker compose exec core pi --version\n```\n";
break;
case "dirty-source":
changed = original.replaceAll("git status --porcelain --untracked-files=all", "git status --short");
break;
case "failed-pull":
changed = original.replace("if ! git pull --ff-only; then abort_update", "if git pull --ff-only; then abort_update");
break;
case "failed-status":
changed = original.replace("if ! RUNNING_PI_VERSION=", "if RUNNING_PI_VERSION=");
break;
case "failed-build":
changed = original.replace("if ! bash scripts/build-local.sh; then", "if bash scripts/build-local.sh; then");
break;
case "same-version-no-selector":
changed = original.replace("TRANSACTIONAL_PI_UPDATE=false", "TRANSACTIONAL_PI_UPDATE=true # unsafe same-version no-op");
break;
case "powershell-source-failure":
changed = original.replace("Assert-NativeSuccess 'Pi status'", "Write-Output 'Pi status unchecked'");
break;
case "failed-export":
changed = original.replace("if ! git checkout-index --all --force", "if git checkout-index --all --force");
break;
case "partial-export":
changed = original.replace("if ! validate_index_export; then", "if validate_index_export; then");
break;
case "mode-120000":
changed = original.replaceAll("120000", "100644-no-symlink-mode");
break;
case "powershell-crlf-failure":
changed = original.replace("Assert-NativeSuccess 'index export'", "Write-Output 'index export unchecked'");
break;
default:
throw new Error(`unknown negative-fixture mutation: ${mutation}`);
}
@@ -129,6 +164,47 @@ expect_guide_rejected \
"$root/docs/install/pi-management.md" docs/install/pi-management.md raw-pi \
"raw non-installation-aware Compose Pi access is forbidden"
expect_guide_rejected \
"dirty or untracked source tree" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md dirty-source \
"installation-aware source update lacks structural token: git status --porcelain --untracked-files=all"
expect_guide_rejected \
"failed source pull" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md failed-pull \
"POSIX source update does not fail closed: source pull"
expect_guide_rejected \
"failed thothctl Pi status" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md failed-status \
"POSIX source update does not fail closed: Pi status"
expect_guide_rejected \
"failed local build" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md failed-build \
"POSIX source update does not fail closed: local build"
expect_guide_rejected \
"same Pi version without durable selector" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md same-version-no-selector \
"POSIX source update lacks the same-version/no-selector path"
expect_guide_rejected \
"PowerShell source command failure propagation" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md powershell-source-failure \
"PowerShell source update does not propagate failure: Pi status"
expect_guide_rejected \
"failed index export" verify_windows_line_endings_guide \
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md failed-export \
"POSIX CRLF repair lacks fail-closed semantic: if ! git checkout-index"
expect_guide_rejected \
"partial index export" verify_windows_line_endings_guide \
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md partial-export \
"POSIX CRLF repair does not prove a complete export before destructive rewrite"
expect_guide_rejected \
"mode 120000 symlink preservation" verify_windows_line_endings_guide \
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md mode-120000 \
"POSIX CRLF repair lacks fail-closed semantic: 120000"
expect_guide_rejected \
"PowerShell CRLF command failure propagation" verify_windows_line_endings_guide \
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md powershell-crlf-failure \
"PowerShell CRLF repair lacks failure propagation: Assert-NativeSuccess 'index export'"
if (( negative_failures != 0 )); then
echo "$negative_failures unsafe installation-document fixtures were accepted" >&2
exit 1