smoke: assert manifest VALUES, not the presence of field names
Lint / actionlint (push) Successful in 16s
Lint / hadolint (push) Successful in 17s

Four build-provenance assertions grepped the manifest for a field name and
never looked at the value:

    run_expect "manifest records pi_version" "cat …manifest.json" '"pi_version"'

which passes on {"pi_version": ""} and on {"pi_version": null}. The tell was
in its own passing output the whole time — `✅ manifest records pi_version (got
"pi_version")` echoes the key back as the thing it claims to have found. Found
while reading run 579's smoke log to confirm v1.8.6's new assertions had really
executed rather than merely gone green.

Now checked against values, and against ground truth where it exists:

- every required component key present, naming the one that vanished
- every component value a full 40-hex SHA (null allowed for pi-studio alone,
  which is legitimately absent in the non-studio variant)
- pi_version equal to `pi --version`, mirroring the mempalace ground-truth check
- release_tag non-empty; source_revision 40-hex and build_date ISO-8601 *when
  populated*, since both default empty on a plain local `docker build` and
  demanding them would fail honest local smoke runs
- --json compared byte-for-byte with the file, which is assertable because that
  mode is a verbatim cat; the old form grepped its output for "release_tag"

Key presence and value shape are deliberately SEPARATE assertions: a single
"all values are valid SHAs" loop passes vacuously on components:{}, because
jq's all() over an empty list is true. Combining them would reproduce the same
shape of hole as the three false greens already recorded in CHANGELOG.md.

Dropped `manifest has no unresolved ('unknown') components`: the 40-hex check
strictly subsumes it ("unknown" is not 40-hex, and only rev() emits it, feeding
components{} exclusively). Removed rather than kept, because a check that can
no longer fail independently is one more green tick that means nothing.

Mutation-tested twice rather than reasoned about: nine fabricated manifests
through the raw jq filters, then twelve through the shipped assertions using
the real `run` helper's `sh -c` quoting path — the quoting is load-bearing,
since a jq filter dying on a quoting error exits non-zero and looks exactly
like a caught defect. Measured on the same twelve defects: old caught 3, missed
9; new catches 12. Three legitimate variations stay green (empty
source_revision, empty build_date, null pi-studio).

Also corrects a factually wrong "Still open" bullet in the released v1.8.6
entry, which claimed pi-devbox-version's human output does not show
mempalace_version and that only --json surfaces it. Both halves are false: it
prints a `palace:` line with live-vs-baked drift annotation, verified against
fabricated manifests (match, skew, and pre-v1.8.6 absent-field cases). Left as
a struck-through correction rather than deleted, since v1.8.6 is published.
This commit is contained in:
2026-08-25 17:15:39 +02:00
parent 93f986e90e
commit cb7b8ad2ae
2 changed files with 155 additions and 15 deletions
+78 -13
View File
@@ -348,12 +348,71 @@ echo ""
echo "── Build provenance ──"
run "/etc/pi-devbox/build-manifest.json present" \
"test -f /etc/pi-devbox/build-manifest.json"
run_expect "manifest records pi-extensions component" \
"cat /etc/pi-devbox/build-manifest.json" '"pi-extensions"'
run_expect "manifest records pi-atelier" \
"cat /etc/pi-devbox/build-manifest.json" '"pi-atelier"'
run_expect "manifest records pi_version" \
"cat /etc/pi-devbox/build-manifest.json" '"pi_version"'
# These next checks replace three that grepped the manifest for the FIELD NAME
# and never looked at the value:
#
# run_expect "manifest records pi_version" "cat …manifest.json" '"pi_version"'
#
# which passes on {"pi_version": ""} and on {"pi_version": null}. The tell was
# visible in its own passing output — `✅ manifest records pi_version (got
# "pi_version")` echoes the key back as the thing it claims to have found.
# Two failure modes were therefore invisible: a key that survives with an empty
# or garbage value, and a key that vanishes from the manifest while every
# remaining value still looks fine.
#
# Those two need SEPARATE assertions, and the reason is a trap worth keeping in
# writing: an "every component value is a valid SHA" loop passes VACUOUSLY on
# components:{} — jq's all() over an empty list is true — so the value check
# alone would go green on a manifest that lost every component. Mutation-tested
# 2026-08-25 across nine fabricated manifests (empty map, deleted key, "",
# null, "unknown", 12-hex truncation, 40 non-hex chars, legit null pi-studio).
run "manifest declares every required component key" '
req="pi-toolkit pi-extensions pi-fork pi-observational-memory pi-atelier mempalace-toolkit pi-studio"
for k in $req; do
jq -e --arg k "$k" "(.components|has(\$k))" /etc/pi-devbox/build-manifest.json >/dev/null \
|| { echo "manifest lost component key: $k" >&2; exit 1; }
done
'
# Subsumes the old `! grep -q \"unknown\"` check ("unknown" is not 40-hex), and
# also catches "", null and truncated SHAs, which that grep let through. null is
# legitimate for pi-studio alone: the non-studio variant has no such clone.
run "manifest component values are resolved 40-hex commits" '
jq -e "
.components
| to_entries
| all(if .key == \"pi-studio\" and .value == null then true
else (.value|type) == \"string\" and (.value|test(\"^[0-9a-f]{40}\$\")) end)
" /etc/pi-devbox/build-manifest.json >/dev/null
'
# pi_version against ground truth, same shape as the mempalace check below.
# Chains with the "pi version matches build arg" assertion earlier in this file:
# together they tie build arg -> installed binary -> recorded manifest, so a
# manifest written from a stale variable cannot pass by agreeing with itself.
run "manifest pi_version matches the installed pi" '
m=$(jq -r ".pi_version // empty" /etc/pi-devbox/build-manifest.json)
b=$(pi --version 2>/dev/null | head -n1 | tr -d "\r")
echo "manifest=[$m] installed=[$b]" >&2
[ -n "$m" ] && [ "$m" = "$b" ]
'
# Top-level provenance fields: assert the SHAPE of each value, and only when the
# field is populated. source_revision and build_date legitimately default to
# empty (Dockerfile.variant ARGs) on a plain local `docker build`, so demanding
# them would fail honest local smoke runs; a populated-but-malformed value is
# the actual defect. release_tag defaults to "dev", so empty means a broken write.
run "manifest top-level fields are well-formed, not merely present" '
j=/etc/pi-devbox/build-manifest.json
t=$(jq -r ".release_tag // empty" $j)
r=$(jq -r ".source_revision // empty" $j)
d=$(jq -r ".build_date // empty" $j)
echo "release_tag=[$t] source_revision=[$r] build_date=[$d]" >&2
[ -n "$t" ] || { echo "release_tag empty (ARG default is dev)" >&2; exit 1; }
if [ -n "$r" ]; then
printf "%s" "$r" | grep -qxE "[0-9a-f]{40}" || { echo "source_revision not a 40-hex commit" >&2; exit 1; }
fi
if [ -n "$d" ]; then
printf "%s" "$d" | grep -qE "^[0-9]{4}-[0-9]{2}-[0-9]{2}T" || { echo "build_date not ISO-8601" >&2; exit 1; }
fi
'
# mempalace CORE was absent from the manifest through v1.8.5: the toolkit SHA
# was recorded but the palace version behind the MCP tools was not, so a palace
# bug could not be correlated to an image version. Assert the field exists AND
@@ -369,17 +428,23 @@ run "manifest mempalace_version matches the installed core" '
[ -n "$m" ] && [ "$m" = "$b" ]
'
# Every component must be a resolved commit (or null for pi-studio in the
# non-studio variant) — 'unknown' means a clone silently failed to resolve.
run "manifest has no unresolved ('unknown') components" \
"! grep -q '\"unknown\"' /etc/pi-devbox/build-manifest.json"
# pi-devbox-version wraps the manifest into a human-first command (this
# PR); verify the binary is present, executable, and both output modes work.
# non-studio variant) — now enforced by the 40-hex value check above, which
# strictly subsumes the old whole-file grep for '"unknown"'. Only rev() ever
# emits "unknown" and rev() feeds components only, so nothing is lost.
# pi-devbox-version wraps the manifest into a human-first command; verify the
# binary is present, executable, and that all three output modes work.
run "pi-devbox-version binary present + executable" \
"test -x /usr/local/bin/pi-devbox-version"
run_expect "pi-devbox-version human output shows release tag" \
"pi-devbox-version" "pi-devbox "
run_expect "pi-devbox-version --json round-trips the manifest" \
"pi-devbox-version --json" '"release_tag"'
# --json is a verbatim `cat` of the manifest, so "round-trips" is assertable
# literally. The old form grepped the output for the string "release_tag" — the
# key name again — which would pass on a truncated or re-serialised dump.
run "pi-devbox-version --json round-trips the manifest byte-for-byte" '
a=$(cat /etc/pi-devbox/build-manifest.json)
b=$(pi-devbox-version --json)
[ "$a" = "$b" ] || { echo "--json output differs from the manifest on disk" >&2; exit 1; }
'
run_expect "pi-devbox-version --quiet is a compact one-liner" \
"pi-devbox-version --quiet | wc -l" "1"
# OCI labels live in the image config, not the container fs — inspect them