Files
stack/scripts/test-discord.sh
T
jason.woltjeandClaude Fable 5.1 1949ed8d31 feat(discord): git verbs for the Discord Sage on the shared-signals root, seat identity through a package credential helper, vault record protocol (#1509)
Row 24. A writable root that is a git work tree may carry a git object in
the binding; the seat then has git_status, git_commit (explicit paths, seat
author, Requested-by trailer from the envelope requester, push at once per
D6), git_pull (ff-only) and git_push (one branch, never force), plus
reserve_id and per-write clone locks under protocol vault. Git children run
with no host config and one credential helper, bin/git-credential.mjs,
reading the 0600 seat token file named in the binding; the fleet helper
serves only the Gitea hosts. Suite 58/58, node 143. rev-code-02 APPROVED
round 1 (#1509 comment 26375, tree 82ab962f).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 07:52:35 -05:00

199 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Offline suite for packages/discord (issue #1509). No token, no network,
# no model: every test drives the connector with a fake gateway, fake REST,
# fake pi and a disposable data root.
#
# scripts/test-discord.sh full run
# NO_COLOR=1 scripts/test-discord.sh plain output
set -uo pipefail
cd "$(dirname "$0")/.."
SANDBOX="$(mktemp -d)"
trap 'rm -rf "$SANDBOX"' EXIT
PASS=0
FAIL=0
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
else
C_OK=""; C_FAIL=""; C_RESET=""
fi
check() {
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
}
echo "toolchain: node $(node --version)"
echo
# --- syntax ---
for f in packages/discord/src/*.mjs packages/discord/bin/*.mjs packages/discord/extension/*.mjs packages/discord/tests/*.mjs packages/discord/fixtures/*.mjs scripts/discord.sh scripts/discord-service.sh; do
case "$f" in
*.sh) bash -n "$f" >/dev/null 2>&1 ;;
*) node --check "$f" >/dev/null 2>&1 ;;
esac
check "syntax: $f" $?
done
# --- no dependencies, no secrets in the package ---
node -e 'const p=require("./packages/discord/package.json"); process.exit(p.dependencies||p.devDependencies?1:0)' >/dev/null 2>&1
check "packages/discord declares no dependencies" $?
if grep -rEn '[MN][A-Za-z0-9]{22,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}' packages/discord scripts/discord.sh >/dev/null 2>&1; then
check "no bot-token-shaped string in packages/discord" 1
else
check "no bot-token-shaped string in packages/discord" 0
fi
node -e '
const b = JSON.parse(require("fs").readFileSync("packages/discord/fixtures/binding.example.json","utf8"));
const ids = JSON.stringify(b).match(/[0-9]{17,20}/g) || [];
process.exit(ids.every((id) => id.startsWith("10000000000000")) ? 0 : 1);
' >/dev/null 2>&1
check "fixture binding uses placeholder ids only" $?
# --- the fixture validates against the schema ---
node -e '
import("./packages/discord/src/binding.mjs").then((m) => {
const raw = JSON.parse(require("fs").readFileSync("packages/discord/fixtures/binding.example.json","utf8"));
m.validateBinding(raw);
}).catch((e) => { console.error(e.message); process.exit(1); });
' >/dev/null 2>&1
check "fixture binding validates" $?
# --- the read-only tools extension against the real pi, offline, no model call ---
# A probe extension loaded next to ours reports pi's active tool list on
# session_start; the flags are the ones buildPiArgs emits with tools.
PI_BIN="$(pwd)/node_modules/.bin/pi"
EXT_DIR="$(pwd)/packages/discord/extension"
PROBE="$SANDBOX/probe-ext.mjs"
cat >"$PROBE" <<'EOF_PROBE'
export default function (pi) {
pi.on("session_start", async () => { process.stderr.write(`PROBE ${JSON.stringify(pi.getActiveTools().sort())}\n`); });
}
EOF_PROBE
mkdir -p "$SANDBOX/toolroot/plans"
echo "row one" >"$SANDBOX/toolroot/plans/QUEUE.md"
TOOLS_JSON="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8}"
PI_COMMON="--mode rpc --no-extensions --no-context-files --no-skills --no-prompt-templates --no-themes --offline --no-session --provider zai --model glm-5.3"
if [ -x "$PI_BIN" ]; then
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_JSON" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search \
>"$SANDBOX/pi-tools.out" 2>"$SANDBOX/pi-tools.err"
grep -qxF 'PROBE ["list_dir","read_file","search"]' "$SANDBOX/pi-tools.err" && grep -q '"command":"get_state","success":true' "$SANDBOX/pi-tools.out"
check "real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool" $?
mkdir -p "$SANDBOX/vault/Businesses"
TOOLS_RW="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"},{\"name\":\"vault\",\"path\":\"$SANDBOX/vault\",\"write\":true}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8}"
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_RW" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search,write_file,edit_file \
>"$SANDBOX/pi-rw.out" 2>"$SANDBOX/pi-rw.err"
grep -qxF 'PROBE ["edit_file","list_dir","read_file","search","write_file"]' "$SANDBOX/pi-rw.err" && grep -q '"command":"get_state","success":true' "$SANDBOX/pi-rw.out" && [ ! -e "$SANDBOX/vault/Businesses/x" ] && [ -z "$(ls -A "$SANDBOX/vault/Businesses")" ]
check "real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start" $?
TOOLS_WEB="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8,\"web\":{\"searxng\":\"http://127.0.0.1:1\"}}"
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_WEB" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search,web_fetch,web_search \
>"$SANDBOX/pi-web.out" 2>"$SANDBOX/pi-web.err"
grep -qxF 'PROBE ["list_dir","read_file","search","web_fetch","web_search"]' "$SANDBOX/pi-web.err" && grep -q '"command":"get_state","success":true' "$SANDBOX/pi-web.out"
check "real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root" $?
# A git root (row 24): a sandbox work tree with an origin, a 0600 token
# file whose content is shaped nothing like a real token.
GITROOT="$SANDBOX/gitroot"
git init -q -b main "$GITROOT" && git -C "$GITROOT" -c user.name=t -c user.email=t@t commit -q --allow-empty -m init
git init -q --bare -b main "$SANDBOX/gitremote.git" && git -C "$GITROOT" remote add origin "$SANDBOX/gitremote.git"
printf 'not_a_real_token_just_a_test_value_x\n' >"$SANDBOX/gittoken" && chmod 0600 "$SANDBOX/gittoken"
GIT_JSON="{\"branch\":\"main\",\"identity\":\"sage\",\"tokenFile\":\"$SANDBOX/gittoken\",\"author\":\"Sage <[email protected]>\"}"
TOOLS_GIT="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"},{\"name\":\"ss\",\"path\":\"$GITROOT\",\"write\":true,\"git\":$GIT_JSON}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8}"
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_GIT" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search,write_file,edit_file,git_status,git_commit,git_pull,git_push \
>"$SANDBOX/pi-git.out" 2>"$SANDBOX/pi-git.err"
grep -qxF 'PROBE ["edit_file","git_commit","git_pull","git_push","git_status","list_dir","read_file","search","write_file"]' "$SANDBOX/pi-git.err" && grep -q '"command":"get_state","success":true' "$SANDBOX/pi-git.out" && [ "$(git -C "$GITROOT" rev-list --count HEAD)" = "1" ] && ! grep -rq 'not_a_real_token' "$SANDBOX/pi-git.out" "$SANDBOX/pi-git.err"
check "real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token" $?
TOOLS_VAULT="{\"roots\":[{\"name\":\"ss\",\"path\":\"$GITROOT\",\"write\":true,\"git\":${GIT_JSON%\}},\"protocol\":\"vault\"}}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8}"
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_VAULT" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search,write_file,edit_file,git_status,git_commit,git_pull,git_push,reserve_id \
>"$SANDBOX/pi-vault.out" 2>"$SANDBOX/pi-vault.err"
grep -qxF 'PROBE ["edit_file","git_commit","git_pull","git_push","git_status","list_dir","read_file","reserve_id","search","write_file"]' "$SANDBOX/pi-vault.err" && grep -q '"command":"get_state","success":true' "$SANDBOX/pi-vault.out"
check "real pi with protocol vault adds reserve_id to the git verbs" $?
TOOLS_GIT_RO="{\"roots\":[{\"name\":\"ss\",\"path\":\"$GITROOT\",\"git\":$GIT_JSON}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8}"
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_GIT_RO" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search \
>"$SANDBOX/pi-gitro.out" 2>"$SANDBOX/pi-gitro.err"
GITRO_RC=$?
[ "$GITRO_RC" -ne 0 ] && grep -q 'git needs write: true' "$SANDBOX/pi-gitro.err" && ! grep -q 'PROBE' "$SANDBOX/pi-gitro.err"
check "real pi refuses a git key on a read-only root (fail closed)" $?
printf '{"type":"get_state","id":"a"}\n' | timeout 60 "$PI_BIN" $PI_COMMON --no-tools --extension "$PROBE" \
>"$SANDBOX/pi-notools.out" 2>"$SANDBOX/pi-notools.err"
grep -qxF 'PROBE []' "$SANDBOX/pi-notools.err"
check "real pi with the pilot flags (--no-tools) exposes no tool at all" $?
printf '{"type":"get_state","id":"a"}\n' | env -u MOSAIC_DISCORD_TOOLS timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search \
>"$SANDBOX/pi-noenv.out" 2>"$SANDBOX/pi-noenv.err"
NOENV_RC=$?
[ "$NOENV_RC" -ne 0 ] && grep -q 'MOSAIC_DISCORD_TOOLS is not set' "$SANDBOX/pi-noenv.err" && ! grep -q 'PROBE' "$SANDBOX/pi-noenv.err" && [ ! -s "$SANDBOX/pi-noenv.out" ]
check "real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)" $?
else
check "pi binary present at node_modules/.bin/pi for the extension checks" 1
fi
# --- the seven offline groups ---
node --test --test-reporter=spec packages/discord/tests/ >"$SANDBOX/node-test.log" 2>&1
NODE_RC=$?
check "node --test packages/discord/tests/ ($(grep -E '^ℹ pass' "$SANDBOX/node-test.log" | tr -d '\n' || echo 'summary missing'))" $NODE_RC
if [ "$NODE_RC" -ne 0 ]; then
grep -E '^✖|not ok|Error|error:' "$SANDBOX/node-test.log" | head -40 >&2
fi
# --- wrapper usage paths ---
scripts/discord.sh --help >/dev/null 2>&1
check "scripts/discord.sh --help exits 0" $?
scripts/discord.sh check >/dev/null 2>&1
[ $? -eq 4 ]
check "scripts/discord.sh check without a binding exits 4" $?
scripts/discord.sh recover >/dev/null 2>&1
[ $? -eq 4 ]
check "scripts/discord.sh recover without a binding exits 4" $?
scripts/discord.sh reload >/dev/null 2>&1
[ $? -eq 4 ]
check "scripts/discord.sh reload without a binding exits 4" $?
# --- the service unit: rendered from the template, never touching systemd here ---
UNITS="$SANDBOX/units"
scripts/discord-service.sh >/dev/null 2>&1
[ $? -eq 4 ]
check "scripts/discord-service.sh without a command exits 4" $?
REPO_DIR="$(pwd)"
scripts/discord-service.sh render >"$SANDBOX/unit.rendered" 2>/dev/null \
&& grep -qF "ExecStart=$REPO_DIR/scripts/discord.sh run %i --supervised" "$SANDBOX/unit.rendered" \
&& ! grep -q '^ExecStartPre=' "$SANDBOX/unit.rendered" \
&& grep -qx 'RestartPreventExitStatus=3' "$SANDBOX/unit.rendered" \
&& grep -qx 'Restart=on-failure' "$SANDBOX/unit.rendered" \
&& grep -qx 'KillSignal=SIGTERM' "$SANDBOX/unit.rendered" \
&& grep -qx 'ExecReload=/bin/kill -HUP $MAINPID' "$SANDBOX/unit.rendered" \
&& ! grep -q '@REPO@\|@PATH@' "$SANDBOX/unit.rendered"
check "service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP" $?
scripts/discord-service.sh install --dir "$UNITS" --no-reload >"$SANDBOX/install.1" 2>&1 \
&& [ -f "$UNITS/[email protected]" ] \
&& grep -q '^written:' "$SANDBOX/install.1" \
&& cmp -s "$UNITS/[email protected]" "$SANDBOX/unit.rendered" \
&& [ "$(stat -c %a "$UNITS/[email protected]")" = "644" ] \
&& [ -z "$(ls -A "$UNITS" | grep -v '^[email protected]$')" ]
check "service install writes the rendered unit (0644) and leaves no temp file" $?
scripts/discord-service.sh install --dir "$UNITS" --no-reload >"$SANDBOX/install.2" 2>&1 && grep -q '^unchanged:' "$SANDBOX/install.2"
check "service install a second time reports unchanged" $?
if command -v systemd-analyze >/dev/null 2>&1; then
cp "$UNITS/[email protected]" "$UNITS/[email protected]"
systemd-analyze --user verify "$UNITS/[email protected]" >"$SANDBOX/verify.log" 2>&1 && ! grep -qi 'warning\|error\|fail' "$SANDBOX/verify.log"
check "systemd-analyze verify accepts the rendered unit" $?
rm -f "$UNITS/[email protected]"
fi
scripts/discord-service.sh uninstall --dir "$UNITS" --no-reload >/dev/null 2>&1 && [ ! -e "$UNITS/[email protected]" ]
check "service uninstall removes the unit file" $?
scripts/discord-service.sh install --dir "$UNITS" --no-reload --bogus >/dev/null 2>&1
[ $? -eq 4 ]
check "service install with an unknown flag exits 4" $?
mkdir -p "$SANDBOX/units-nouser"
env -u USER scripts/discord-service.sh install --dir "$SANDBOX/units-nouser" --no-reload >"$SANDBOX/install.nouser" 2>&1 \
&& [ -f "$SANDBOX/units-nouser/[email protected]" ] \
&& grep -qF "enable-linger $(id -un)" "$SANDBOX/install.nouser"
check "service install with USER unset finishes and names the account for lingering" $?
echo
echo "discord suite: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]