Files
stack/scripts/test-discord.sh
T
jason.woltjeandClaude Opus 5.5 08bd3a4cc5 fix(tests): clear NODE_TEST_CONTEXT for nested node --test in two suites (#1508)
test-foundation.sh and test-discord.sh ran a nested node --test that would
exit 0 on failure under a parent runner. Both clear the variable now, and
each has a check that fails the suite if it comes back. Darkwing wrote it,
Filbert approved it (e464be6c). Nine suites green on an index export.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 19:10:31 -05:00

215 lines
15 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" $?
printf 'not_a_real_key_just_a_test_value_x\n' >"$SANDBOX/setspark.key" && chmod 0600 "$SANDBOX/setspark.key"
TOOLS_SETSPARK="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"maxFileBytes\":4096,\"maxCallsPerTurn\":8,\"setspark\":{\"baseUrl\":\"https://api.setspark.invalid\",\"keyFile\":\"$SANDBOX/setspark.key\",\"principal\":\"sage\"}}"
printf '{"type":"get_state","id":"a"}\n' | MOSAIC_DISCORD_TOOLS="$TOOLS_SETSPARK" timeout 60 "$PI_BIN" $PI_COMMON --no-builtin-tools \
--extension "$EXT_DIR/tools.mjs" --extension "$PROBE" --tools list_dir,read_file,search,record_list,record_get,record_create,record_update,resolve_id,open_approval_request,get_approval_request,create_document \
>"$SANDBOX/pi-setspark.out" 2>"$SANDBOX/pi-setspark.err"
grep -qxF 'PROBE ["create_document","get_approval_request","list_dir","open_approval_request","read_file","record_create","record_get","record_list","record_update","resolve_id","search"]' "$SANDBOX/pi-setspark.err" && grep -q '"command":"get_state","success":true' "$SANDBOX/pi-setspark.out" && ! grep -rq 'not_a_real_key' "$SANDBOX/pi-setspark.out" "$SANDBOX/pi-setspark.err"
check "real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key" $?
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 ---
# A nested `node --test` inherits a parent runner's NODE_TEST_CONTEXT, reports
# to that runner and exits 0 whatever its tests do, so the suite clears it
# (#1508 N13). The planted failing test proves a failure still fails here.
node_tests() { env -u NODE_TEST_CONTEXT node --test "$@"; }
mkdir -p "$SANDBOX/planted"
printf '%s\n' 'import { test } from "node:test";' 'import assert from "node:assert/strict";' 'test("planted failure", () => assert.equal(1, 2));' >"$SANDBOX/planted/planted.test.mjs"
NODE_TEST_CONTEXT=child-v8 node_tests "$SANDBOX/planted/" >"$SANDBOX/planted.log" 2>&1
[ $? -eq 1 ] && grep -q '^✖ planted failure' "$SANDBOX/planted.log"
check "a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT" $?
node_tests --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 ]