# XCE project — test targets
# Usage:
#   make test          # unit tests only (no server needed, CGO_ENABLED=0)
#   make test-backend  # backend CGO conversion tests (requires nix dev shell + build dir)
#   make test-integration  # regression suite against live server
#   make test-all      # unit + integration tests

XCE_BIN   ?= /tmp/xce_static
SRV_URL   ?= http://localhost:8080
GOFLAGS   := CGO_ENABLED=0

# build-time-versioning task 4.3: dev-loop -ldflags path. Computes commit id
# (git, -dirty suffixed) and build time (UTC) directly -- independent of
# meson -- mirroring xce-build.sh's (task 2.1) git/dirty-check logic and
# passing the identical -ldflags shape go_build (task 4.1) appends. `:=`
# (simply expanded: the $(shell ...) calls run once, immediately, when this
# line is parsed) rather than `=` (recursively expanded: would re-invoke git
# and date on every reference to these variables) -- this computes each
# value exactly once per `make` invocation, matching xce-build.sh's own
# single-computation-per-invocation model, so build-server and build-cli
# (and multiple references within one recipe) all see the same values.
GIT_COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null)
GIT_STATUS      := $(shell git status --porcelain 2>/dev/null)
ifeq ($(GIT_COMMIT_HASH),)
# git itself failed (e.g. no .git present) -- Requirement 1.4/4.1's fallback
# discipline: record as unknown rather than fail the build.
GIT_COMMIT := unknown
else ifeq ($(GIT_STATUS),)
GIT_COMMIT := $(GIT_COMMIT_HASH)
else
GIT_COMMIT := $(GIT_COMMIT_HASH)-dirty
endif
BUILD_TIME      := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

# dirty-content-fingerprint task 4.3: independent DIRTY_FINGERPRINT
# computation for this same dev-loop -ldflags path -- a NEW, sibling value
# alongside GIT_COMMIT_HASH/GIT_STATUS/GIT_COMMIT/BUILD_TIME above, which
# this block does not read or modify. Mirrors xce-build.sh's (task 2.1)
# dirty_fingerprint algorithm exactly: empty when the tree is clean, or when
# the hash computation itself fails (never fabricated -- Requirement 5.1);
# otherwise a sha256 prefix (8 hex chars, matching xce-build.sh's
# `cut -c1-8`) over `git diff HEAD` concatenated with the sorted raw content
# of every file `git ls-files -z --others --exclude-standard` lists.
#
# DIRTY_FP_SRC_DIR mirrors xce-build.sh's own SRC_DIR (its script's parent
# directory, i.e. src/ -- one level above this Makefile's src/frontend/):
# `git status --porcelain`/`git diff HEAD` are repo-wide with root-relative
# path display regardless of invocation cwd, but a plain, pathspec-less
# `git ls-files --others` is genuinely SCOPED to (and shows paths relative
# to) the invocation cwd -- confirmed empirically. Running every git call
# from DIRTY_FP_SRC_DIR (via `cd`, not just `git -C`, since the untracked
# filenames `git ls-files` prints must also resolve on disk relative to that
# same directory for `cat` to read them) reproduces xce-build.sh's exact
# untracked-file scope regardless of where `make` itself is invoked from --
# required for this task's Observable ("same fingerprint ... as a
# meson-built binary from the identical tree state").
#
# Uses `git ls-files -z ... | sort -z | xargs -0 -r cat --` instead of
# xce-build.sh's bash `while IFS= read -r -d '' f; do cat -- "$f"; done`
# loop -- both concatenate the same sorted files' raw bytes in the same
# order (verified byte-identical), but this form needs no embedded
# single-quote characters (bash's `-d ''`), which would otherwise be
# fragile to escape inside a Make `$(shell ...)` argument. A real mktemp
# scratch file (not command-substitution capture) collects the combined
# diff+file bytes before hashing, so no trailing-newline stripping skews
# the digest versus xce-build.sh's own temp-file-based approach. Zero
# working-tree/index mutation -- reads only.
#
# `git diff HEAD` and the ls-files/sort/xargs step are gated as SEPARATE,
# individually-checked steps (fp_ok, mirroring xce-build.sh's own fp_ok
# tracking) rather than one `{ cmd1; cmd2; } >"$$fp_tmp"` group -- a POSIX
# sequence/group's exit status reflects only its LAST command, so a grouped
# form would silently swallow a `git diff HEAD` failure whenever the
# ls-files/sort/xargs step still succeeded, hashing incomplete data into a
# fabricated non-empty fingerprint instead of degrading to empty
# (Requirement 5.1).
#
# round-2 remediation: `git ls-files -z --others --exclude-standard` is ALSO
# pulled out of any pipe and run as its own directly-redirected command into
# a second temp file (list_tmp), with its OWN individually-checked `$?` --
# it is no longer the first stage of a 3-stage
# `ls-files | sort -z | xargs -0 -r cat --` pipe. That pipe had the exact
# same class of bug as the grouped-form one above: a POSIX pipe's exit
# status reflects only its LAST stage, and `sort -z`/`xargs -0 -r` both
# exit 0 on the empty stdin a failed `git ls-files` produces, so a
# `git ls-files` failure was silently swallowed and fp_tmp was hashed with
# only the `git diff HEAD` bytes in it -- a real but fabricated/incomplete
# fingerprint, not the required empty fallback.
#
# Two options were considered: (a) prefix this whole $(shell ...) body with
# `set -o pipefail;`, which empirically works in THIS repo's dev shell
# (GNU Make's $(shell) here runs under /bin/sh, itself a symlink into the
# nix store's bash-interactive package -- confirmed via `ls -la /bin/sh`),
# or (b) the restructuring below, which sidesteps pipe-exit propagation
# entirely instead of depending on it. (b) was chosen: this same repo's own
# `.kiro/steering/build-deployment.md` documents this exact codebase having
# been bitten before by code that silently depended on "/bin/sh happens to
# be symlinked to bash" (deploy-ha.sh's shebang was found locally altered to
# `#!/bin/sh` while still using bash-only syntax, and only kept working by
# that coincidence) -- its own fix was to stop relying on that coincidence
# rather than to keep depending on it, and this Makefile does not pin
# `SHELL := bash` to turn the coincidence into a guarantee. Since $(shell)
# here has no such pin either, (a)'s `pipefail` would rely on the identical
# unpinned coincidence for a build-portability property (dirty-fingerprint
# accuracy) rather than a dev-loop-only convenience script, so (b) is the
# more conservative choice. `git`, the highest-risk command (failure here
# is exactly the case under test), is now entirely outside any pipe. The
# residual `LC_ALL=C sort -z <"$$list_tmp" | xargs -0 -r cat --` pipe keeps
# a narrower gap -- a `sort` failure specifically could still be masked by
# `xargs`'s own (last-stage) exit status -- but `xargs -r cat --` DOES
# propagate an individual `cat` failure (e.g. an unreadable file `git
# ls-files` listed): GNU xargs exits non-zero (123) whenever any invoked
# command exits 1-125, so that sub-case is still caught without pipefail.
# This matches the reviewer's own assessment: a `sort` failure is a far
# lower-risk residual than the `git` failure this fix targets.
DIRTY_FP_SRC_DIR  := $(CURDIR)/..
DIRTY_FINGERPRINT := $(shell \
	cd "$(DIRTY_FP_SRC_DIR)" 2>/dev/null && \
	if [ -n "$$(git status --porcelain 2>/dev/null)" ]; then \
		fp_tmp=$$(mktemp 2>/dev/null); \
		if [ -n "$$fp_tmp" ]; then \
			fp_ok=1; \
			list_tmp=""; \
			if ! git diff HEAD >"$$fp_tmp" 2>/dev/null; then fp_ok=0; fi; \
			if [ "$$fp_ok" = 1 ]; then \
				list_tmp=$$(mktemp 2>/dev/null); \
				if [ -z "$$list_tmp" ]; then \
					fp_ok=0; \
				elif ! git ls-files -z --others --exclude-standard >"$$list_tmp" 2>/dev/null; then \
					fp_ok=0; \
				fi; \
			fi; \
			if [ "$$fp_ok" = 1 ] && ! LC_ALL=C sort -z <"$$list_tmp" | xargs -0 -r cat -- >>"$$fp_tmp" 2>/dev/null; then fp_ok=0; fi; \
			if [ "$$fp_ok" = 1 ]; then \
				fp=$$(sha256sum <"$$fp_tmp" 2>/dev/null | cut -c1-8); \
				if [ -n "$$fp" ]; then printf "%s" "$$fp"; fi; \
			fi; \
			rm -f "$$fp_tmp" "$$list_tmp" 2>/dev/null; \
		fi; \
	fi)

VERSION_LDFLAGS := -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.BuildTime=$(BUILD_TIME) -X main.DirtyFingerprint=$(DIRTY_FINGERPRINT)"

# bug-1558: test-backend's CGO_CFLAGS/CGO_LDFLAGS, self-derived via
# pkg-config so `make test-backend` works standalone instead of only inside
# meson's custom_target env (apiserver/meson.build is the source of truth
# this mirrors -- librtti/c-capnproto/openssl/tinylog resolve via
# pkg-config there; math/rt are cc.find_library() results with no
# pkg-config package, added directly as -lm/-lrt to match).
# BUILD_DIR defaults to the tree tools/xce-build.sh produces (profile asan);
# override for a non-ASAN build, e.g.
#   make test-backend BUILD_DIR=$(CURDIR)/../build.dbg FRONTEND_CGO_SANITIZE=
BUILD_DIR             ?= $(CURDIR)/../xce.build
FRONTEND_CGO_PKGS     := librtti c-capnproto openssl tinylog
FRONTEND_CGO_SANITIZE ?= -fsanitize=address -g
FRONTEND_CGO_CFLAGS   := $(FRONTEND_CGO_SANITIZE) $(shell pkg-config --cflags $(FRONTEND_CGO_PKGS)) -I$(CURDIR)/../include -I$(BUILD_DIR)/lib
FRONTEND_CGO_LDFLAGS  := $(FRONTEND_CGO_SANITIZE) $(shell pkg-config --libs $(FRONTEND_CGO_PKGS)) -lm -lrt -L$(BUILD_DIR)/lib -lxce

.PHONY: build-cli build-server test test-backend test-integration test-all

build-cli:
	cd xcecli && $(GOFLAGS) go build $(VERSION_LDFLAGS) -o $(XCE_BIN) ./cmd/main.go

build-server:
	cd apiserver && go build $(VERSION_LDFLAGS) -o /tmp/apiserver ./cmd/main.go

## Run all Go unit tests that do not require CGO or a live server.
test:
	@echo "=== Unit tests: apiserver ==="
	cd apiserver && $(GOFLAGS) go test ./store/... ./utils/... ./controllers/... ./backend/... -count=1
	@echo ""
	@echo "=== Unit tests: xcecli ==="
	cd xcecli && $(GOFLAGS) go test ./... -count=1

## Run backend CGO conversion tests. Standalone-runnable: CGO_CFLAGS/
## CGO_LDFLAGS are self-derived above via pkg-config (bug-1558) -- no manual
## env setup needed. Still requires the nix develop shell (for pkg-config's
## .pc files) and a prior `xce-build.sh lib` (or any component that builds
## libxce.a) so BUILD_DIR/lib/libxce.a and BUILD_DIR/lib's generated headers
## exist.
test-backend:
	@echo "=== Backend CGO tests: apiserver/backend ==="
	cd apiserver && CGO_ENABLED=1 \
	  CGO_CFLAGS="$(FRONTEND_CGO_CFLAGS)" \
	  CGO_LDFLAGS="$(FRONTEND_CGO_LDFLAGS)" \
	  go test ./backend/... -count=1

## Run regression suite against a live server (requires XCE_BIN and SRV_URL)
test-integration: build-cli
	@echo "=== Integration regression: $(SRV_URL) ==="
	XCE=$(XCE_BIN) URL=$(SRV_URL) bash tests/regression/run.sh

## Run everything
test-all: test test-integration
