Files
ThothII/scripts/test-verify-workspace-install-docs.sh
T

290 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regression test for copyable installation examples and secret-path validation.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
output="$(mktemp "${TMPDIR:-/tmp}/thoth-install-docs-test.XXXXXX")"
verifier_functions="$(mktemp "${TMPDIR:-/tmp}/thoth-install-docs-functions.XXXXXX")"
negative_root="$(mktemp -d "${TMPDIR:-/tmp}/thoth-install-docs-negative.XXXXXX")"
trap 'rm -f "$output" "$verifier_functions"; rm -rf "$negative_root"' EXIT HUP INT TERM
"$root/scripts/verify-workspace-install-docs.sh" --fixtures-only >"$output"
for fixture in \
"local installation guide contract" \
"source update fail-closed semantics" \
"Windows line-ending recovery guide contract" \
"Pi management guide contract" \
"server installation guide contract" \
"Nginx reverse-proxy guide contract" \
"Caddy reverse-proxy guide contract" \
"local installation example rendered from path with spaces" \
"server installation example rendered from path with spaces" \
"local manual canonical base+override references" \
"server manual canonical base+override references" \
"canonical local base+override fixture" \
"canonical server base+override fixture" \
"relative secret-source fixture rejected" \
"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
exit 1
}
done
for manual in "$root/docs/install/local-workspace-registry.md"; do
grep -Fq 'export THT_SOURCE_ROOT=/absolute/path/to/ThothII' "$manual" || {
echo "installation manual does not publish a self-contained THT_SOURCE_ROOT export: $manual" >&2
exit 1
}
grep -Fq 'export THT_WORKSPACE_BINDINGS_ENV_FILE=' "$manual" || {
echo "installation manual does not publish a self-contained bindings export: $manual" >&2
exit 1
}
if rg -n 'source[[:space:]]+\.env' "$manual"; then
echo "installation manual unsafely imports operator .env: $manual" >&2
exit 1
fi
done
grep -Fq 'THTCTL=/srv/thothii/operator/thothctl' \
"$root/docs/install/server-workspace-registry.md" || {
echo "server installation manual does not use the installation-aware operator CLI" >&2
exit 1
}
grep -Fq 'INSTALLATION=/srv/thothii/operator/thothii-installation.yaml' \
"$root/docs/install/server-workspace-registry.md" || {
echo "server installation manual does not identify the server installation descriptor" >&2
exit 1
}
if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|connector-secrets\.workspace-registry|docker compose' \
"$root/docs/install/local-workspace-registry.md" \
"$root/docs/install/server-workspace-registry.md"; then
echo "installation manuals still document a bypassed Compose or copied connector override path" >&2
exit 1
fi
# Load only the verifier's function definitions so each deliberately unsafe guide can be checked
# in isolation without invoking Docker-backed Compose fixtures.
sed '/^case "\$mode" in/,$d' "$root/scripts/verify-workspace-install-docs.sh" >"$verifier_functions"
# shellcheck source=/dev/null
source "$verifier_functions"
negative_failures=0
expect_guide_rejected() {
local label="$1" validator="$2" source_guide="$3" relative_path="$4"
local mutation="$5" expected_error="$6"
local fixture_root="$negative_root/${label// /-}"
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);
const original = fs.readFileSync(path, "utf8");
let changed = original;
switch (mutation) {
case "durable-selector":
changed = original.replaceAll("--source build", "--source stale-build");
break;
case "dangerous-volumes":
changed = original.replace("Do **not** run `docker compose down --volumes`", "Run `docker compose down --volumes`");
break;
case "incomplete-powershell":
changed = original.replaceAll("icacls.exe", "Write-Output");
break;
case "broken-crlf":
changed = original.replaceAll("git checkout-index --all --force --prefix=", "git add --renormalize . # ");
break;
case "raw-pi":
changed += "\n```sh\ndocker compose exec core pi --version\n```\n";
break;
case "server-secret-env":
changed += "\n```dotenv\nTHT_MODEL_API_KEY=unsafe-secret-value\n```\n";
break;
case "server-docker-socket":
changed += "\nMount /var/run/docker.sock into core for management.\n";
break;
case "server-coupling":
changed += "\nAttach core to the omics_portal application network.\n";
break;
case "nginx-no-auth":
changed = original.replace(" auth_request /_authenticate;", " # authentication omitted");
break;
case "nginx-core-upstream":
changed = original.replaceAll("http://127.0.0.1:8080", "http://127.0.0.1:8787");
break;
case "nginx-no-sse":
changed = original.replace(" proxy_buffering off;", " proxy_buffering on;");
break;
case "caddy-no-auth":
changed = original.replace("forward_auth auth-gateway:4180 {", "# forward authentication omitted");
break;
case "caddy-client-identity":
changed = original.replace("X-Thoth-Principal-Subject>X-Thoth-Trusted-Principal-Subject", "X-Thoth-Principal-Subject");
break;
case "caddy-core-upstream":
changed = original.replaceAll("127.0.0.1:8080", "127.0.0.1:8787");
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}`);
}
if (changed === original) throw new Error(`negative-fixture mutation made no change: ${mutation}`);
fs.writeFileSync(path, changed);
NODE
set +e
(root="$fixture_root"; set -e; "$validator") >"$fixture_output" 2>&1
local status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "negative fixture accepted: $label" >&2
cat "$fixture_output" >&2
negative_failures=$((negative_failures + 1))
elif ! grep -Fq -- "$expected_error" "$fixture_output"; then
echo "negative fixture failed for the wrong reason: $label" >&2
cat "$fixture_output" >&2
negative_failures=$((negative_failures + 1))
fi
}
expect_guide_rejected \
"durable selector keeps old core" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md durable-selector \
"installation-aware source update lacks structural token: --source build"
expect_guide_rejected \
"dangerous down volumes instruction" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md dangerous-volumes \
"docker compose down --volumes must appear only in an explicit prose prohibition"
expect_guide_rejected \
"incomplete native PowerShell path" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md incomplete-powershell \
"native PowerShell setup lacks structural token: icacls.exe"
expect_guide_rejected \
"renormalize leaves CRLF worktree bytes" verify_windows_line_endings_guide \
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md broken-crlf \
"Windows line-ending guide lacks required instruction: git checkout-index --all --force"
expect_guide_rejected \
"raw non-installation-aware Pi access" verify_pi_management_guide \
"$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 \
"server secret in environment" verify_server_guide \
"$root/docs/install/server.md" docs/install/server.md server-secret-env \
"server installation guide embeds a secret value"
expect_guide_rejected \
"server Docker socket mount" verify_server_guide \
"$root/docs/install/server.md" docs/install/server.md server-docker-socket \
"server installation guide introduces a Docker socket dependency"
expect_guide_rejected \
"server application coupling" verify_server_guide \
"$root/docs/install/server.md" docs/install/server.md server-coupling \
"server installation guide introduces forbidden application coupling"
expect_guide_rejected \
"Nginx identity without authentication" verify_reverse_proxy_nginx_guide \
"$root/docs/install/reverse-proxy-nginx.md" docs/install/reverse-proxy-nginx.md nginx-no-auth \
"Nginx proxy lacks structural token: auth_request /_authenticate;"
expect_guide_rejected \
"Nginx direct core exposure" verify_reverse_proxy_nginx_guide \
"$root/docs/install/reverse-proxy-nginx.md" docs/install/reverse-proxy-nginx.md nginx-core-upstream \
"Nginx proxy must forward only to frontend on 127.0.0.1:8080"
expect_guide_rejected \
"Nginx buffered SSE" verify_reverse_proxy_nginx_guide \
"$root/docs/install/reverse-proxy-nginx.md" docs/install/reverse-proxy-nginx.md nginx-no-sse \
"Nginx proxy lacks structural token: proxy_buffering off;"
expect_guide_rejected \
"Caddy identity without authentication" verify_reverse_proxy_caddy_guide \
"$root/docs/install/reverse-proxy-caddy.md" docs/install/reverse-proxy-caddy.md caddy-no-auth \
"Caddy proxy lacks structural token: forward_auth auth-gateway:4180 {"
expect_guide_rejected \
"Caddy untrusted identity forwarding" verify_reverse_proxy_caddy_guide \
"$root/docs/install/reverse-proxy-caddy.md" docs/install/reverse-proxy-caddy.md caddy-client-identity \
"Caddy proxy lacks structural token: X-Thoth-Principal-Subject>X-Thoth-Trusted-Principal-Subject"
expect_guide_rejected \
"Caddy direct core exposure" verify_reverse_proxy_caddy_guide \
"$root/docs/install/reverse-proxy-caddy.md" docs/install/reverse-proxy-caddy.md caddy-core-upstream \
"Caddy proxy must forward only to frontend on 127.0.0.1:8080"
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
fi
echo "unsafe installation-document fixtures rejected passed"