test(dirty-content-fingerprint): add startup-log-unchanged consistency check
Adds src/tools/test-dirty-fingerprint-startup-log-unchanged.sh, reusing task 6.1's already-approved isolated-git-worktree technique. Builds scheduler and apiserver for real on a genuinely dirty tree, starts both daemons, and proves --version/-version show a real fingerprint while their startup logs show the unchanged, fingerprint-free original format. Apiserver's injected fingerprint reuses scheduler's own extracted value (same dirty tree) rather than an independent recomputation -- verified independently by review as legitimate, not tautological: apiserver has no fingerprint-computation code of its own (only ever receives an externally injected value via -ldflags), and the test still exercises apiserver's own real composeVersionDisplay()/logger.Info code on a genuinely running binary, with a round-trip check that would catch a real strip-and-append bug. Verified independently by review with a fresh full run plus a separately- authored 4-case discrimination probe outside the script's own demo block. No leftover processes, worktrees, or build dirs after either run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a7d64c8225
commit
a47854184a
2 changed files with 640 additions and 3 deletions
|
|
@ -101,9 +101,10 @@
|
|||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1_
|
||||
- _Depends: 2.1, 3.2_
|
||||
|
||||
- [ ] 6.2 Startup-log-unchanged consistency test
|
||||
- For at least one C daemon (scheduler) and apiserver, on a dirty tree, capture the startup log's version line and separately run `--version`/`-version`; assert the startup log line is byte-identical to the original `build-time-versioning` feature's already-shipped format (no fingerprint present), while `--version`/`-version` does show the fingerprint
|
||||
- Observable: a passing test proves the fingerprint never leaks into the startup log for either artifact, even while the tree is dirty
|
||||
- [x] 6.2 Startup-log-unchanged consistency test
|
||||
- New script `src/tools/test-dirty-fingerprint-startup-log-unchanged.sh` reuses task 6.1's already-approved isolated-git-worktree technique; builds scheduler and apiserver for real on a genuinely dirty tree, starts both daemons, captures `--version`/`-version` (expect fingerprint) and each startup log (expect old fingerprint-free format)
|
||||
- Apiserver's injected fingerprint value is reused from scheduler's own extracted value (same dirty tree, same content) rather than independently recomputed -- independently assessed by review as legitimate, not tautological: apiserver has no independent fingerprint-computation code of its own in production (only ever receives an externally-injected value via `-ldflags`), the script still exercises apiserver's own real, unmocked `composeVersionDisplay()`/`logger.Info` code paths on a genuinely running compiled binary, and a round-trip `api_fp == sched_fp` check would catch a real strip-and-append bug
|
||||
- Observable confirmed live (independently re-verified by review, fresh full run + a separately-authored 4-case discrimination probe outside the script's own demo block): both daemons' `--version`/`-version` show real fingerprints while their startup logs show the unchanged, fingerprint-free old format; no leftover processes, worktrees, or build dirs after either run
|
||||
- _Requirements: 3.1, 3.2_
|
||||
- _Depends: 3.2, 4.4_
|
||||
|
||||
|
|
|
|||
636
src/tools/test-dirty-fingerprint-startup-log-unchanged.sh
Executable file
636
src/tools/test-dirty-fingerprint-startup-log-unchanged.sh
Executable file
|
|
@ -0,0 +1,636 @@
|
|||
#!/usr/bin/env bash
|
||||
# test-dirty-fingerprint-startup-log-unchanged.sh — dirty-content-fingerprint
|
||||
# task 6.2 (.kiro/specs/dirty-content-fingerprint/tasks.md, Requirements
|
||||
# 3.1, 3.2)
|
||||
#
|
||||
# MOTIVATING GAP this guards against: this feature's whole design hinges on
|
||||
# a deliberate divergence between two previously-identical call sites --
|
||||
# per-binary `--version`/`-version` output now composes a content-derived
|
||||
# fingerprint (task 3.1-4.4), but the startup INFO log line each daemon
|
||||
# already emits (shipped by the earlier, separate `build-time-versioning`
|
||||
# feature) must stay completely untouched, fingerprint-free, even while the
|
||||
# tree is dirty and even while that same running binary's `--version` output
|
||||
# clearly does show a fingerprint. Task 6.1's sibling script
|
||||
# (test-dirty-fingerprint-distinguishability.sh) already proves the
|
||||
# `--version`-side behaviour end-to-end; task 6.2 is the complementary proof
|
||||
# that the fingerprint never leaks into the OTHER call site -- a regression
|
||||
# here (e.g. a future edit accidentally threading dirty_fingerprint into the
|
||||
# startup-log composition call) would not be caught by 6.1 at all, since 6.1
|
||||
# never inspects a startup log.
|
||||
#
|
||||
# Components checked (one C daemon, one Go binary, matching task 6.2's own
|
||||
# text and mirroring test-version-startup-consistency.sh's precedent):
|
||||
# - xce_scheduler (C daemon, task 3.2's `xce_compose_version_ex()` path)
|
||||
# - xce_apiserver (Go binary, task 4.4's `composeVersionDisplay()` path)
|
||||
#
|
||||
# ISOLATION FROM AMBIENT SESSION NOISE / DIRTY-TREE CONSTRUCTION: same
|
||||
# rationale and same technique as task 6.1's sibling script -- this repo is
|
||||
# routinely under continuous, unrelated concurrent editing in this
|
||||
# environment, so a controlled, genuinely-dirty tree cannot be reliably
|
||||
# constructed by editing the ambient shared checkout. This script creates
|
||||
# its own throwaway, fully-isolated `git worktree` at HEAD (detached),
|
||||
# registers a `trap cleanup EXIT` BEFORE creating it (safe no-op if creation
|
||||
# never happened), and makes its one deliberate edit only inside that
|
||||
# isolated worktree -- the real repo's own working tree is never touched
|
||||
# (verified explicitly at the end, same as task 6.1's script).
|
||||
#
|
||||
# Build path notes (mirrors test-version-startup-consistency.sh, task 6.1's
|
||||
# sibling for build-time-versioning):
|
||||
# - xce_scheduler is built via the real `xce-build.sh scheduler debug`
|
||||
# entry point, INSIDE the isolated worktree, so its own real
|
||||
# dirty_fingerprint computation (task 2.1) runs against genuinely
|
||||
# dirty, controlled content.
|
||||
# - xce_apiserver's two documented real build paths (build-server /
|
||||
# go_build) are both pre-existing-broken/unusable in this sandbox
|
||||
# (bug-1597 / goproxy.cn hang, see test-version-startup-consistency.sh's
|
||||
# header for the full analysis, unchanged and independently
|
||||
# reconfirmed here) -- this script uses the same documented workaround:
|
||||
# a direct `go build -ldflags "-X main.GitCommit=... -X
|
||||
# main.BuildTime=... -X main.DirtyFingerprint=..."` invocation. The
|
||||
# DirtyFingerprint value injected is the REAL value extracted from the
|
||||
# scheduler binary's own `--version` output captured moments earlier in
|
||||
# this same run (same isolated worktree, same dirty content, same
|
||||
# dirty_fingerprint algorithm task 2.1 already computed once) rather
|
||||
# than a second, independently-reimplemented hash computation here --
|
||||
# this is both simpler (no algorithm duplication risk) and MORE
|
||||
# faithful to what a real `xce-build.sh apiserver` invocation would
|
||||
# inject (the exact same computed value, reused for every component in
|
||||
# one build session) than recomputing it a second time would be.
|
||||
# GitCommit is composed the same way xce-build.sh's own commit_id
|
||||
# computation would for a dirty tree: "<short-sha>-dirty".
|
||||
#
|
||||
# Usage:
|
||||
# bash src/tools/test-dirty-fingerprint-startup-log-unchanged.sh [--keep-worktree]
|
||||
#
|
||||
# Exit code is the sole pass/fail signal (0 = pass, non-zero = a real
|
||||
# fingerprint leak into a startup log, an unexpected startup-log format
|
||||
# change, or a demonstration inconsistency). Two real builds (scheduler +
|
||||
# apiserver's C lib dependency) plus a direct go build plus two real daemon
|
||||
# startups happen -- budget several minutes of wall-clock time.
|
||||
#
|
||||
# Why this file lives here: same convention as test-build-version-
|
||||
# regression.sh / test-version-startup-consistency.sh /
|
||||
# test-dirty-fingerprint-distinguishability.sh -- a standalone,
|
||||
# CLI-runnable, real-build-and-real-process regression check for a
|
||||
# cross-cutting concern, deliberately NOT a meson test() (real builds +
|
||||
# real daemon startups are too expensive for every `meson test` run).
|
||||
#
|
||||
# Revalidation triggers:
|
||||
# - SCHED_VERSION_PREFIX below must match the literal prefix passed to
|
||||
# xce_compose_version()/xce_compose_version_ex() in scheduler/src/main.c
|
||||
# (tasks 3.2/task 3.1's XCE_DIRTY_SUFFIX).
|
||||
# - APISERVER_VERSION_PREFIX below must match the literal
|
||||
# "xce-apiserver " format string in
|
||||
# src/frontend/apiserver/cmd/main.go's -version branch (task 4.4).
|
||||
# - APISERVER_LOG_ANCHOR below must match the literal
|
||||
# "********** Initialize Logger done!" log message in the same file.
|
||||
# - XCE_DIRTY_SUFFIX below ("-dirty") must match the literal suffix text
|
||||
# in src/lib/utils.c's XCE_DIRTY_SUFFIX and apiserver/cmd/main.go's
|
||||
# composeVersionDisplay()'s strings.TrimSuffix() call.
|
||||
set -euo pipefail
|
||||
|
||||
# ── paths ───────────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WORKTREE="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
SCHED_VERSION_PREFIX="xce v0.0.0.1-"
|
||||
APISERVER_VERSION_PREFIX="xce-apiserver "
|
||||
APISERVER_LOG_ANCHOR="Initialize Logger done!"
|
||||
XCE_DIRTY_SUFFIX="-dirty"
|
||||
|
||||
BUILD_DIR_SCHED="startuplog.sched"
|
||||
BUILD_DIR_APILIB="startuplog.apilib"
|
||||
README_REL="README.md"
|
||||
MARKER="<!-- xce-dirty-fingerprint-test: task 6.2 startup-log-unchanged marker -->"
|
||||
|
||||
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 / test-dirty-fingerprint-
|
||||
# distinguishability.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=""
|
||||
SCHED_PID=""
|
||||
API_PID=""
|
||||
SCHED_BIN=""
|
||||
APISERVER_BIN=""
|
||||
|
||||
cleanup() {
|
||||
# Kill any daemons this run started FIRST, before the leftover-process
|
||||
# scan and before the isolated worktree (and its binaries) is removed.
|
||||
if [ -n "$SCHED_PID" ]; then
|
||||
kill -9 "$SCHED_PID" 2>/dev/null || true
|
||||
wait "$SCHED_PID" 2>/dev/null || true
|
||||
SCHED_PID=""
|
||||
fi
|
||||
if [ -n "$API_PID" ]; then
|
||||
kill -9 "$API_PID" 2>/dev/null || true
|
||||
wait "$API_PID" 2>/dev/null || true
|
||||
API_PID=""
|
||||
fi
|
||||
|
||||
# Leftover-process scan: resolve each /proc/<pid>/exe symlink and
|
||||
# compare against the REAL built binary paths, rather than grepping
|
||||
# /proc/<pid>/cmdline text or using `pgrep -f`/`pkill -f` -- this
|
||||
# sandbox's Bash tool wraps every invocation as a shell command whose
|
||||
# own argv can contain this very script's literal text, which makes a
|
||||
# cmdline-text-based match self-match the invoking wrapper (see
|
||||
# cerebrum.md bug-1576). /proc/<pid>/exe resolution has no such hazard.
|
||||
if [ -n "$SCHED_BIN" ] || [ -n "$APISERVER_BIN" ]; then
|
||||
for pid_dir in /proc/[0-9]*; do
|
||||
[ -d "$pid_dir" ] || continue
|
||||
pid="${pid_dir#/proc/}"
|
||||
exe_path="$(readlink -f "$pid_dir/exe" 2>/dev/null || true)"
|
||||
[ -n "$exe_path" ] || continue
|
||||
if [ "$exe_path" = "$SCHED_BIN" ] || [ "$exe_path" = "$APISERVER_BIN" ]; then
|
||||
red "LEFTOVER PROCESS after cleanup: pid=$pid exe=$exe_path -- force-killing."
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
# ── field extraction (verbatim, proven helpers from
|
||||
# test-version-startup-consistency.sh) ──────────────────────────────────────
|
||||
#
|
||||
# extract_composed_fields TEXT PREFIX
|
||||
# TEXT is either a raw --version string, or a larger line of text (e.g. a
|
||||
# full log line) that CONTAINS "<PREFIX><commit> built <build_time>"
|
||||
# somewhere in it. Prints "<commit>\t<build_time>" on stdout; returns 1 if
|
||||
# PREFIX is not found at all.
|
||||
extract_composed_fields() {
|
||||
local text="$1" prefix="$2" rest commit build_time
|
||||
rest="${text##*"$prefix"}"
|
||||
if [ "$rest" = "$text" ]; then
|
||||
return 1
|
||||
fi
|
||||
rest="$(printf '%s' "$rest" | tr -d '\r\n')"
|
||||
commit="${rest%% built *}"
|
||||
build_time="${rest#* built }"
|
||||
printf '%s\t%s\n' "$commit" "$build_time"
|
||||
}
|
||||
|
||||
# extract_zap_fields LOGLINE
|
||||
# apiserver's startup log line carries the two values as structured zap
|
||||
# JSON fields ("gitCommit": "...", "buildTime": "...") rather than a
|
||||
# composed string. Prints "<commit>\t<build_time>"; returns 1 if either
|
||||
# field is absent. Deliberately POSIX BRE `sed` -- see
|
||||
# test-version-startup-consistency.sh's identical helper for why (this
|
||||
# script's real invocation environment resolves plain `grep`/`sed` to
|
||||
# BusyBox with no -P support).
|
||||
extract_zap_fields() {
|
||||
local logline="$1" commit build_time
|
||||
commit="$(printf '%s' "$logline" | sed -n 's/.*"gitCommit":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
build_time="$(printf '%s' "$logline" | sed -n 's/.*"buildTime":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
if [ -z "$commit" ] || [ -z "$build_time" ]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s\t%s\n' "$commit" "$build_time"
|
||||
}
|
||||
|
||||
# ── the two validating functions: one confirms --version DOES show a real
|
||||
# fingerprint (not the old "-dirty" format), the other confirms the startup
|
||||
# log does NOT (byte-identical commit field to the original shipped format,
|
||||
# and the fingerprint string absent anywhere in the line). Both the real
|
||||
# per-binary checks below AND demonstrate_detection_capability() (the
|
||||
# discriminating-power proof) exercise these exact functions. ─────────────
|
||||
|
||||
# assert_version_shows_fingerprint LABEL RAW COMPONENT(sched|apiserver) BASE_COMMIT
|
||||
# On success: prints PASS, sets FP_OUT to the extracted fingerprint suffix,
|
||||
# returns 0. On failure: prints FAIL with a clear reason, resets FP_OUT to
|
||||
# "", returns 1.
|
||||
FP_OUT=""
|
||||
assert_version_shows_fingerprint() {
|
||||
local label="$1" raw="$2" component="$3" base_commit="$4"
|
||||
local prefix fields commit_field
|
||||
FP_OUT=""
|
||||
|
||||
case "$component" in
|
||||
sched) prefix="$SCHED_VERSION_PREFIX" ;;
|
||||
apiserver) prefix="$APISERVER_VERSION_PREFIX" ;;
|
||||
*)
|
||||
red "FAIL [$label]: unknown component '$component'"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! fields="$(extract_composed_fields "$raw" "$prefix")"; then
|
||||
red "FAIL [$label]: --version/-version output did not contain the expected prefix '$prefix'. Raw: [$raw]"
|
||||
return 1
|
||||
fi
|
||||
commit_field="${fields%%$'\t'*}"
|
||||
|
||||
if [ "$commit_field" = "${base_commit}${XCE_DIRTY_SUFFIX}" ]; then
|
||||
red "FAIL [$label]: --version/-version shows the OLD '$XCE_DIRTY_SUFFIX' format ('$commit_field') -- expected a real content-derived fingerprint suffix on this deliberately dirty tree. Raw: [$raw]"
|
||||
return 1
|
||||
fi
|
||||
if [ "$commit_field" = "$base_commit" ]; then
|
||||
red "FAIL [$label]: --version/-version shows the bare commit id with no suffix at all ('$commit_field') -- expected a fingerprint suffix on this deliberately dirty tree. Raw: [$raw]"
|
||||
return 1
|
||||
fi
|
||||
case "$commit_field" in
|
||||
"${base_commit}-"*)
|
||||
FP_OUT="${commit_field#"${base_commit}-"}"
|
||||
;;
|
||||
*)
|
||||
red "FAIL [$label]: commit field '$commit_field' does not match expected base commit '$base_commit' with a fingerprint suffix. Raw: [$raw]"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
green "PASS [$label]: --version/-version shows a real fingerprint suffix (not '$XCE_DIRTY_SUFFIX') -- commit_field='$commit_field' fingerprint='$FP_OUT'"
|
||||
return 0
|
||||
}
|
||||
|
||||
# assert_startup_log_unchanged LABEL LOG_LINE COMPONENT(sched|apiserver) BASE_COMMIT FINGERPRINT
|
||||
# The exact comparison Requirement 3.2 requires: the startup log's commit
|
||||
# field must be byte-identical to the original build-time-versioning
|
||||
# feature's already-shipped dirty-tree format ("<base_commit>-dirty"), and
|
||||
# the fingerprint string must not appear ANYWHERE in the log line (not just
|
||||
# absent from the commit field -- a leak via some other field would be just
|
||||
# as real a regression).
|
||||
assert_startup_log_unchanged() {
|
||||
local label="$1" log_line="$2" component="$3" base_commit="$4" fingerprint="$5"
|
||||
local fields commit_field expected_commit="${base_commit}${XCE_DIRTY_SUFFIX}"
|
||||
local ok=1
|
||||
|
||||
case "$component" in
|
||||
sched)
|
||||
if ! fields="$(extract_composed_fields "$log_line" "$SCHED_VERSION_PREFIX")"; then
|
||||
red "FAIL [$label]: scheduler startup log line did not contain the expected prefix '$SCHED_VERSION_PREFIX'. Line: [$log_line]"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
apiserver)
|
||||
if ! fields="$(extract_zap_fields "$log_line")"; then
|
||||
red "FAIL [$label]: apiserver startup log line did not contain both gitCommit/buildTime zap fields. Line: [$log_line]"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
red "FAIL [$label]: unknown component '$component'"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
commit_field="${fields%%$'\t'*}"
|
||||
|
||||
if [ "$commit_field" != "$expected_commit" ]; then
|
||||
red "FAIL [$label]: startup log commit field is '$commit_field' -- expected it byte-identical to the original build-time-versioning feature's already-shipped format '$expected_commit' (unchanged, no fingerprint). Line: [$log_line]"
|
||||
ok=0
|
||||
fi
|
||||
|
||||
if [ -n "$fingerprint" ]; then
|
||||
case "$log_line" in
|
||||
*"$fingerprint"*)
|
||||
red "FAIL [$label]: startup log line contains the fingerprint '$fingerprint' -- it must NEVER appear in the startup log (Requirement 3.2). Line: [$log_line]"
|
||||
ok=0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$ok" = 1 ]; then
|
||||
green "PASS [$label]: startup log unchanged from original format (commit field='$expected_commit'), fingerprint '$fingerprint' does not leak into it."
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Proves assert_startup_log_unchanged() -- the EXACT function the real
|
||||
# checks below use -- would catch every required leak/regression mode:
|
||||
# fingerprint leaking straight into the commit field, fingerprint leaking
|
||||
# via some OTHER field on the same line while the commit field itself looks
|
||||
# fine, and (for completeness) a commit field that silently drifted from
|
||||
# the original shipped format even without the fingerprint being involved
|
||||
# at all. Also proves genuinely well-formed, non-leaking lines still PASS.
|
||||
demonstrate_detection_capability() {
|
||||
local base="$1" real_fp="$2"
|
||||
|
||||
yellow "=== DEMONSTRATION: proving assert_startup_log_unchanged() catches every required leak/regression mode ==="
|
||||
|
||||
yellow "--- (a) scheduler: fingerprint leaks straight into the commit field (synthetic) ---"
|
||||
local bad_sched_commit="2026-08-21 12:00:00 INFO xce v0.0.0.1-${base}-${real_fp} built 2026-08-21T12:00:00Z scheduler starting, election candidates=3"
|
||||
if assert_startup_log_unchanged "DEMO-sched-commit-leak" "$bad_sched_commit" "sched" "$base" "$real_fp"; then
|
||||
red "DEMONSTRATION FAILED: incorrectly PASSED a scheduler startup log line whose commit field carries the fingerprint."
|
||||
exit 1
|
||||
fi
|
||||
green "Confirmed: a fingerprint leaking straight into the scheduler startup log's commit field is correctly rejected."
|
||||
|
||||
yellow "--- (b) apiserver: fingerprint leaks straight into the gitCommit field (synthetic) ---"
|
||||
local bad_api_commit="{\"level\":\"INFO\",\"message\":\"********** Initialize Logger done!\",\"gitCommit\": \"${base}-${real_fp}\",\"buildTime\": \"2026-08-21T12:00:00Z\"}"
|
||||
if assert_startup_log_unchanged "DEMO-api-commit-leak" "$bad_api_commit" "apiserver" "$base" "$real_fp"; then
|
||||
red "DEMONSTRATION FAILED: incorrectly PASSED an apiserver startup log line whose gitCommit field carries the fingerprint."
|
||||
exit 1
|
||||
fi
|
||||
green "Confirmed: a fingerprint leaking straight into the apiserver startup log's gitCommit field is correctly rejected."
|
||||
|
||||
yellow "--- (c) apiserver: gitCommit field itself is unchanged ('-dirty'), but the fingerprint leaks via a DIFFERENT field on the same line (synthetic) ---"
|
||||
local bad_api_elsewhere="{\"level\":\"INFO\",\"message\":\"********** Initialize Logger done!\",\"gitCommit\": \"${base}-dirty\",\"buildTime\": \"2026-08-21T12:00:00Z\",\"extra\":\"${real_fp}\"}"
|
||||
if assert_startup_log_unchanged "DEMO-api-elsewhere-leak" "$bad_api_elsewhere" "apiserver" "$base" "$real_fp"; then
|
||||
red "DEMONSTRATION FAILED: incorrectly PASSED a log line where the fingerprint leaked via an unrelated field, even though gitCommit itself looked fine."
|
||||
exit 1
|
||||
fi
|
||||
green "Confirmed: a fingerprint leaking anywhere in the line (not just the commit field) is correctly rejected."
|
||||
|
||||
yellow "--- (d) scheduler: commit field silently drifted from the original shipped format, independent of any fingerprint leak ---"
|
||||
local bad_sched_drift="2026-08-21 12:00:00 INFO xce v0.0.0.1-${base} built 2026-08-21T12:00:00Z scheduler starting, election candidates=3"
|
||||
if assert_startup_log_unchanged "DEMO-sched-format-drift" "$bad_sched_drift" "sched" "$base" "$real_fp"; then
|
||||
red "DEMONSTRATION FAILED: incorrectly PASSED a scheduler startup log line whose commit field is missing the original '-dirty' suffix entirely."
|
||||
exit 1
|
||||
fi
|
||||
green "Confirmed: a commit field that drifted from the original shipped format is correctly rejected, even with no fingerprint present at all."
|
||||
|
||||
yellow "--- (e) sanity: genuinely unchanged, non-leaking log lines must still PASS ---"
|
||||
local good_sched="2026-08-21 12:00:00 INFO xce v0.0.0.1-${base}-dirty built 2026-08-21T12:00:00Z scheduler starting, election candidates=3"
|
||||
if ! assert_startup_log_unchanged "DEMO-sanity-sched" "$good_sched" "sched" "$base" "$real_fp"; then
|
||||
red "DEMONSTRATION FAILED: incorrectly FAILED a genuinely unchanged scheduler startup log line."
|
||||
exit 1
|
||||
fi
|
||||
local good_api="{\"level\":\"INFO\",\"message\":\"********** Initialize Logger done!\",\"gitCommit\": \"${base}-dirty\",\"buildTime\": \"2026-08-21T12:00:00Z\"}"
|
||||
if ! assert_startup_log_unchanged "DEMO-sanity-api" "$good_api" "apiserver" "$base" "$real_fp"; then
|
||||
red "DEMONSTRATION FAILED: incorrectly FAILED a genuinely unchanged apiserver startup log line."
|
||||
exit 1
|
||||
fi
|
||||
green "Confirmed: genuinely unchanged, non-leaking startup log lines pass for both components."
|
||||
|
||||
green "=== DEMONSTRATION complete: a fingerprint leak (into the commit field, or via any other field on the line) and a silent commit-field format drift are ALL rejected; genuinely unchanged log lines are correctly accepted ==="
|
||||
}
|
||||
|
||||
overall_pass=1
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# Set up the isolated worktree and make it genuinely dirty
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
ISO_DIR="$(mktemp -u "${TMPDIR:-/tmp}/xce-startuplog-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
|
||||
printf '\n%s\n' "$MARKER" >> "$readme_path"
|
||||
|
||||
dirty_status="$(git -C "$ISO_DIR" status --porcelain)"
|
||||
if [ -z "$dirty_status" ]; then
|
||||
red "FATAL: isolated worktree is still clean after the deliberate edit -- cannot construct the dirty-tree scenario this test requires."
|
||||
exit 1
|
||||
fi
|
||||
yellow "Isolated worktree is now genuinely dirty (by construction, one controlled edit to $README_REL):"
|
||||
printf '%s\n' "$dirty_status"
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# xce_scheduler (C daemon): build, capture --version, start, capture startup
|
||||
# log line
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
yellow "=== [scheduler] nix develop $ISO_SRC --command bash $ISO_SRC/tools/xce-build.sh scheduler debug $BUILD_DIR_SCHED ==="
|
||||
nix develop "$ISO_SRC" --command bash "$ISO_SRC/tools/xce-build.sh" scheduler debug "$BUILD_DIR_SCHED"
|
||||
|
||||
SCHED_BIN="$ISO_SRC/$BUILD_DIR_SCHED/scheduler/xce_scheduler"
|
||||
if [ ! -x "$SCHED_BIN" ]; then
|
||||
red "FATAL: expected binary not found/executable at $SCHED_BIN after a successful-looking build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sched_version_raw="$("$SCHED_BIN" --version 2>&1)"
|
||||
yellow "[scheduler] --version raw output: [$sched_version_raw]"
|
||||
|
||||
sched_fp=""
|
||||
if assert_version_shows_fingerprint "scheduler-version" "$sched_version_raw" "sched" "$base_commit"; then
|
||||
sched_fp="$FP_OUT"
|
||||
else
|
||||
overall_pass=0
|
||||
fi
|
||||
|
||||
sched_log="$ISO_DIR/scheduler.log"
|
||||
rm -f "$sched_log"
|
||||
"$SCHED_BIN" --logfile "$sched_log" >"$ISO_DIR/scheduler.stdout" 2>&1 &
|
||||
SCHED_PID=$!
|
||||
|
||||
sched_log_line=""
|
||||
waited=0
|
||||
while [ "$waited" -lt 20 ]; do
|
||||
if [ -f "$sched_log" ] && grep -qF -- "$SCHED_VERSION_PREFIX" "$sched_log" 2>/dev/null; then
|
||||
sched_log_line="$(grep -F -- "$SCHED_VERSION_PREFIX" "$sched_log" | tail -1)"
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
kill -9 "$SCHED_PID" 2>/dev/null || true
|
||||
wait "$SCHED_PID" 2>/dev/null || true
|
||||
SCHED_PID=""
|
||||
|
||||
if [ -z "$sched_log_line" ]; then
|
||||
red "FAIL [scheduler-startup-log]: startup log never produced a line containing '$SCHED_VERSION_PREFIX' within 10s. Log contents:"
|
||||
cat "$sched_log" 2>/dev/null >&2 || true
|
||||
overall_pass=0
|
||||
else
|
||||
yellow "[scheduler] startup log line: [$sched_log_line]"
|
||||
if [ -n "$sched_fp" ]; then
|
||||
if ! assert_startup_log_unchanged "scheduler-startup-log" "$sched_log_line" "sched" "$base_commit" "$sched_fp"; then
|
||||
overall_pass=0
|
||||
fi
|
||||
else
|
||||
yellow "[scheduler] skipping startup-log-unchanged assertion: no real fingerprint was captured from --version above."
|
||||
overall_pass=0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# xce_apiserver (Go binary): build lib dep, build via direct go build
|
||||
# reusing the scheduler's real fingerprint, capture -version, start, capture
|
||||
# startup log line
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
yellow "=== [apiserver] building lib dependency: nix develop $ISO_SRC --command bash $ISO_SRC/tools/xce-build.sh lib debug $BUILD_DIR_APILIB ==="
|
||||
nix develop "$ISO_SRC" --command bash "$ISO_SRC/tools/xce-build.sh" lib debug "$BUILD_DIR_APILIB"
|
||||
|
||||
APILIB_DIR="$ISO_SRC/$BUILD_DIR_APILIB"
|
||||
if [ ! -f "$APILIB_DIR/lib/libxce.a" ]; then
|
||||
red "FATAL: expected $APILIB_DIR/lib/libxce.a not found after 'lib' build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_COMMIT_ID="${base_commit}${XCE_DIRTY_SUFFIX}"
|
||||
API_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
API_DIRTY_FINGERPRINT="$sched_fp"
|
||||
|
||||
if [ -z "$API_DIRTY_FINGERPRINT" ]; then
|
||||
red "FATAL: no real fingerprint available from the scheduler build above to inject into the apiserver build -- cannot construct this test's required scenario (a real, non-degraded fingerprint)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
yellow "=== [apiserver] go build (direct invocation, see header comment re: bug-1597 / go_build's goproxy.cn hang in this sandbox) -- commit_id=$API_COMMIT_ID build_time=$API_BUILD_TIME dirty_fingerprint=$API_DIRTY_FINGERPRINT ==="
|
||||
APISERVER_BIN="$APILIB_DIR/frontend/apiserver/xce_apiserver"
|
||||
mkdir -p "$(dirname "$APISERVER_BIN")"
|
||||
nix develop "$ISO_SRC" --command bash -c "
|
||||
set -e
|
||||
cd '$ISO_SRC/frontend/apiserver'
|
||||
CGO_CFLAGS=\"-g \$(pkg-config --cflags librtti c-capnproto openssl tinylog) -I$ISO_SRC/include -I$APILIB_DIR/lib\"
|
||||
CGO_LDFLAGS=\"-g \$(pkg-config --libs librtti c-capnproto openssl tinylog) -lm -lrt -L$APILIB_DIR/lib -lxce\"
|
||||
GOPROXY=off CGO_ENABLED=1 CGO_CFLAGS=\"\$CGO_CFLAGS\" CGO_LDFLAGS=\"\$CGO_LDFLAGS\" \
|
||||
go build -mod=readonly -gcflags 'all=-N -l' -ldflags \"-X main.GitCommit=$API_COMMIT_ID -X main.BuildTime=$API_BUILD_TIME -X main.DirtyFingerprint=$API_DIRTY_FINGERPRINT\" \
|
||||
-o $APISERVER_BIN ./cmd/main.go
|
||||
"
|
||||
|
||||
if [ ! -x "$APISERVER_BIN" ]; then
|
||||
red "FATAL: expected binary not found/executable at $APISERVER_BIN after a successful-looking build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api_version_raw="$("$APISERVER_BIN" -version 2>&1)"
|
||||
yellow "[apiserver] -version raw output: [$api_version_raw]"
|
||||
|
||||
api_fp=""
|
||||
if assert_version_shows_fingerprint "apiserver-version" "$api_version_raw" "apiserver" "$base_commit"; then
|
||||
api_fp="$FP_OUT"
|
||||
else
|
||||
overall_pass=0
|
||||
fi
|
||||
if [ -n "$api_fp" ] && [ "$api_fp" != "$sched_fp" ]; then
|
||||
red "FAIL [apiserver-version]: injected fingerprint '$sched_fp' but -version reports '$api_fp' -- the injection did not round-trip correctly."
|
||||
overall_pass=0
|
||||
fi
|
||||
|
||||
# Minimal topdir so apiserver reaches its startup INFO log line without a
|
||||
# real controller cluster -- identical technique to
|
||||
# test-version-startup-consistency.sh (deploy-ha.sh's own lightweight
|
||||
# stub-mode setup): config/{auth.yaml,rbac.yaml} copied verbatim,
|
||||
# app.yaml with its trailing `backend:` section stripped so main.go boots
|
||||
# in permanent stub mode, and a freshly-generated logger.yaml pointing at
|
||||
# this run's own ISO_DIR.
|
||||
APISERVER_SRC_CONFIG="$ISO_SRC/frontend/apiserver/config"
|
||||
API_TOPDIR="$ISO_DIR/apiserver_topdir"
|
||||
mkdir -p "$API_TOPDIR/config"
|
||||
cp "$APISERVER_SRC_CONFIG/auth.yaml" "$API_TOPDIR/config/auth.yaml"
|
||||
cp "$APISERVER_SRC_CONFIG/rbac.yaml" "$API_TOPDIR/config/rbac.yaml"
|
||||
sed '/^backend:/,$d' "$APISERVER_SRC_CONFIG/app.yaml" > "$API_TOPDIR/config/app.yaml"
|
||||
cat > "$API_TOPDIR/config/logger.yaml" << EOF
|
||||
logging:
|
||||
level: "debug"
|
||||
encoding: console
|
||||
outputPath: "$ISO_DIR/apiserver.log"
|
||||
file:
|
||||
maxSize: 100
|
||||
maxBackups: 10
|
||||
maxAge: 14
|
||||
compress: true
|
||||
encoderConfig:
|
||||
messageKey: "message"
|
||||
levelKey: "level"
|
||||
timeKey: "timestamp"
|
||||
nameKey: "logger"
|
||||
callerKey: "caller"
|
||||
functionKey: "func"
|
||||
stacktraceKey: "stacktrace"
|
||||
timeEncoder: "ISO8601"
|
||||
levelEncoder: "capital"
|
||||
EOF
|
||||
|
||||
api_log="$ISO_DIR/apiserver.log"
|
||||
rm -f "$api_log"
|
||||
"$APISERVER_BIN" -d "$API_TOPDIR" >"$ISO_DIR/apiserver.stdout" 2>&1 &
|
||||
API_PID=$!
|
||||
|
||||
api_log_line=""
|
||||
waited=0
|
||||
while [ "$waited" -lt 20 ]; do
|
||||
if [ -f "$api_log" ] && grep -qF -- "$APISERVER_LOG_ANCHOR" "$api_log" 2>/dev/null; then
|
||||
api_log_line="$(grep -F -- "$APISERVER_LOG_ANCHOR" "$api_log" | head -1)"
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
kill -9 "$API_PID" 2>/dev/null || true
|
||||
wait "$API_PID" 2>/dev/null || true
|
||||
API_PID=""
|
||||
|
||||
if [ -z "$api_log_line" ]; then
|
||||
red "FAIL [apiserver-startup-log]: startup log never produced a line containing '$APISERVER_LOG_ANCHOR' within 10s. Log contents:"
|
||||
cat "$api_log" 2>/dev/null >&2 || true
|
||||
cat "$ISO_DIR/apiserver.stdout" 2>/dev/null >&2 || true
|
||||
overall_pass=0
|
||||
else
|
||||
yellow "[apiserver] startup log line: [$api_log_line]"
|
||||
if [ -n "$api_fp" ]; then
|
||||
if ! assert_startup_log_unchanged "apiserver-startup-log" "$api_log_line" "apiserver" "$base_commit" "$api_fp"; then
|
||||
overall_pass=0
|
||||
fi
|
||||
else
|
||||
yellow "[apiserver] skipping startup-log-unchanged assertion: no real fingerprint was captured from -version above."
|
||||
overall_pass=0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── discrimination demonstration ────────────────────────────────────────────
|
||||
demo_fp="${sched_fp:-$api_fp}"
|
||||
if [ -n "$demo_fp" ]; then
|
||||
demonstrate_detection_capability "$base_commit" "$demo_fp"
|
||||
else
|
||||
red "FATAL: neither binary produced a usable real fingerprint to demonstrate the comparison logic against."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── constraint check: the SHARED working tree's own README.md must be
|
||||
# completely untouched by this run (all edits 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: xce_scheduler and xce_apiserver both show a real content-derived fingerprint in --version/-version on a dirty tree, while their startup log stays byte-identical to the original build-time-versioning format with no fingerprint leak ==="
|
||||
exit 0
|
||||
else
|
||||
red "=== FAILURE DETECTED: see FAIL line(s) above ==="
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue