diff --git a/.kiro/specs/dirty-content-fingerprint/tasks.md b/.kiro/specs/dirty-content-fingerprint/tasks.md index 08a5b4e8..d72ffbff 100644 --- a/.kiro/specs/dirty-content-fingerprint/tasks.md +++ b/.kiro/specs/dirty-content-fingerprint/tasks.md @@ -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_ diff --git a/src/tools/test-dirty-fingerprint-startup-log-unchanged.sh b/src/tools/test-dirty-fingerprint-startup-log-unchanged.sh new file mode 100755 index 00000000..c95fe53b --- /dev/null +++ b/src/tools/test-dirty-fingerprint-startup-log-unchanged.sh @@ -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: "-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="" + +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//exe symlink and + # compare against the REAL built binary paths, rather than grepping + # /proc//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//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 " built " +# somewhere in it. Prints "\t" 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 "\t"; 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 ("-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