11e512e497
pi reads custom providers from ~/.pi/agent/models.json, and ~/.pi is the devbox-pi-config named volume. A file baked into the image would only seed a fresh volume, once. A hand-made link is one more step on every new volume: on 2026-10-12 that was the same `ln -s /opt/fleet-pi/models.json …` once per device, for one fleet-shared file. New rootfs/usr/local/lib/pi-devbox/link-pi-models.sh, called from entrypoint-user.sh after the pi-toolkit and pi-extensions installs. When /opt/fleet-pi/models.json exists (PI_MODELS_JSON overrides the path, =off disables it), it links ~/.pi/agent/models.json to it, honouring PI_CODING_AGENT_DIR. It only ever creates a link where nothing exists: a real file or a link of the user's own is left alone, with one NOTE line. With nothing mounted, it does nothing and prints nothing. A link whose target later vanishes is harmless; measured with pi 1.0.0, a dangling models.json behaves like a missing one (rc 0, no error, no custom models). Tests: - scripts/test-link-pi-models.sh: 13 two-sided cases against a throwaway HOME. Wired into lint.yml, so it runs on every push and not only at release. - Of 11 mutations, 10 are caught. The 11th (ln -s -> ln -sfn on the final link) is unreachable behind the guards. - smoke-test.sh: with nothing mounted no link appears, and the script links a mounted file. Both commands were run under dash here. - recreate-sanity-check.sh: pass, warn or fail on the link, but only when a shared file is mounted. Here it passes. Docs: - README "Custom model providers (models.json)": mount the DIRECTORY (git pull renames, and a single-file mount then shows "No such file" until recreate), keep keys in .env as "$NAME". - .env.example: PI_MODELS_JSON. Local gates green: lint-shell, check-workflow-shell, check-dockerfile-directives, check-base-hash, check-doc-drift.
101 lines
5.5 KiB
Bash
Executable File
101 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-link-pi-models.sh — two-sided tests for rootfs/usr/local/lib/pi-devbox/link-pi-models.sh.
|
|
#
|
|
# Runs the real script against a throwaway HOME / PI_CODING_AGENT_DIR, so it never
|
|
# touches a live ~/.pi/agent. Every "must link" case has a "must NOT touch" twin:
|
|
# the script's whole contract is that it only ever creates a link where nothing
|
|
# exists, so the negative cases are the important half.
|
|
#
|
|
# bash scripts/test-link-pi-models.sh # CI (lint.yml) and locally
|
|
# LINK_PI_MODELS=/path/to/variant.sh bash scripts/test-link-pi-models.sh
|
|
set -u
|
|
here="$(cd "$(dirname "$0")/.." && pwd)"
|
|
SCRIPT="${LINK_PI_MODELS:-$here/rootfs/usr/local/lib/pi-devbox/link-pi-models.sh}"
|
|
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
|
PASS=0; FAIL=0
|
|
ok() { printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
|
|
bad() { printf ' ❌ %s — %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
|
|
|
|
# One fresh world per case: shared dir with models.json, empty HOME, agent dir unset.
|
|
world() {
|
|
rm -rf "$T/w"; mkdir -p "$T/w/shared" "$T/w/home"
|
|
printf '{"providers":{}}\n' > "$T/w/shared/models.json"
|
|
SRC="$T/w/shared/models.json"; AG="$T/w/home/.pi/agent"; DEST="$AG/models.json"
|
|
}
|
|
# run [env assignments...] — run the script with HOME and PI_CODING_AGENT_DIR pointed into the world.
|
|
# "$@" goes FIRST so an option like `-u PI_MODELS_JSON` is still parsed as one by env.
|
|
run() { OUT="$(env "$@" HOME="$T/w/home" PI_CODING_AGENT_DIR="$AG" bash "$SCRIPT" 2>&1)"; RC=$?; }
|
|
|
|
echo "── link-pi-models.sh ($SCRIPT)"
|
|
|
|
world; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ -L "$DEST" ] && [ "$(readlink "$DEST")" = "$SRC" ] && [[ "$OUT" == *linked* ]]; then
|
|
ok "links models.json when the shared file exists and nothing is there (creates the agent dir)"
|
|
else bad "link when absent" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
|
|
|
run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ -z "$OUT" ] && [ "$(readlink "$DEST")" = "$SRC" ]; then
|
|
ok "second run is a silent no-op (every container start runs it)"
|
|
else bad "idempotent" "rc=$RC out=[$OUT]"; fi
|
|
|
|
world; mkdir -p "$AG"; printf 'my own\n' > "$DEST"; before="$(sha256sum "$DEST")"; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ ! -L "$DEST" ] && [ "$(sha256sum "$DEST")" = "$before" ] && [[ "$OUT" == NOTE*"real file"* ]]; then
|
|
ok "never replaces a real models.json (content unchanged, one NOTE line)"
|
|
else bad "real file kept" "rc=$RC islink=$([ -L "$DEST" ] && echo y) out=[$OUT]"; fi
|
|
|
|
world; mkdir -p "$AG"; ln -s /somewhere/else.json "$DEST"; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ "$(readlink "$DEST")" = /somewhere/else.json ] && [[ "$OUT" == NOTE*"left alone"* ]]; then
|
|
ok "never re-points a link the user made to another file"
|
|
else bad "foreign link kept" "rc=$RC link=$(readlink "$DEST") out=[$OUT]"; fi
|
|
|
|
world; rm -f "$SRC"; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
|
ok "no shared file → creates nothing and says nothing (the default for most users)"
|
|
else bad "absent source" "rc=$RC out=[$OUT]"; fi
|
|
|
|
world; rm -f "$SRC"; mkdir -p "$SRC"; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ]; then
|
|
ok "a directory at the path is not a models.json → nothing"
|
|
else bad "directory source" "rc=$RC out=[$OUT]"; fi
|
|
|
|
for v in off OFF ""; do
|
|
world; run PI_MODELS_JSON="$v"
|
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
|
ok "PI_MODELS_JSON='$v' disables it"
|
|
else bad "disable '$v'" "rc=$RC out=[$OUT]"; fi
|
|
done
|
|
|
|
world; run PI_MODELS_JSON="$SRC"; rm -f "$SRC"; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ -L "$DEST" ] && [ "$(readlink "$DEST")" = "$SRC" ] && [ -z "$OUT" ]; then
|
|
ok "our link whose target vanished (mount removed) is left as is, silently (pi treats it as no file)"
|
|
else bad "dangling own link" "rc=$RC out=[$OUT]"; fi
|
|
|
|
# pi honours PI_CODING_AGENT_DIR, so the link must land THERE, not in $HOME/.pi/agent.
|
|
world; AG="$T/w/elsewhere/agent"; DEST="$AG/models.json"; run PI_MODELS_JSON="$SRC"
|
|
if [ "$RC" = 0 ] && [ "$(readlink "$DEST" 2>/dev/null)" = "$SRC" ] && [ ! -e "$T/w/home/.pi/agent/models.json" ]; then
|
|
ok "links into PI_CODING_AGENT_DIR when set (and leaves \$HOME/.pi/agent alone)"
|
|
else bad "PI_CODING_AGENT_DIR" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
|
|
|
# 'off' must be a switch, not merely a path that happens not to exist: with a file
|
|
# literally named "off" in the working directory it must still disable.
|
|
world; printf '{}\n' > "$T/w/off"; OUT="$(cd "$T/w" && env PI_MODELS_JSON=off HOME="$T/w/home" PI_CODING_AGENT_DIR="$AG" bash "$SCRIPT" 2>&1)"; RC=$?
|
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ]; then
|
|
ok "PI_MODELS_JSON=off disables it even when a file named 'off' exists in the cwd"
|
|
else bad "off is a switch" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
|
|
|
# The default path is /opt/fleet-pi/models.json. Whether it exists depends on where
|
|
# this runs (a fleet devbox: yes; CI: no), so derive the expectation, don't assume it.
|
|
world; run -u PI_MODELS_JSON
|
|
if [ -f /opt/fleet-pi/models.json ]; then
|
|
if [ "$RC" = 0 ] && [ "$(readlink "$DEST" 2>/dev/null)" = /opt/fleet-pi/models.json ]; then
|
|
ok "PI_MODELS_JSON unset → default /opt/fleet-pi/models.json (present here) is linked"
|
|
else bad "default path (present)" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
|
else
|
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
|
ok "PI_MODELS_JSON unset → default /opt/fleet-pi/models.json (absent here) → nothing"
|
|
else bad "default path (absent)" "rc=$RC out=[$OUT]"; fi
|
|
fi
|
|
|
|
echo "── $PASS passed, $FAIL failed"
|
|
[ "$FAIL" = 0 ]
|