molecule-core/tests/e2e/test_chat_attachments_e2e.sh
Hongming Wang 6125700c39 test(e2e): plug /tmp scratch leaks in 3 shell E2E tests + add CI lint gate (RFC #2873 iter 2)
Three shell E2E tests created scratch files via `mktemp` but never
deleted them on early exit (assertion failure, SIGINT, errexit). Each
CI run leaked ~10-100 KB of /tmp into the runner; over ~200 runs/week
that's 20+ MB of accumulated cruft.

## Files

- **test_chat_attachments_e2e.sh** — was missing both trap and rm;
  added per-run TMPDIR_E2E with `trap rm -rf … EXIT INT TERM`.
- **test_notify_attachments_e2e.sh** — had a `cleanup()` for the
  workspace but didn't include the TMPF; only an unconditional
  `rm -f` at the bottom (line 233) which doesn't fire on early exit.
  Extended cleanup() to also rm the scratch + dropped the redundant
  trailing rm.
- **test_chat_attachments_multiruntime_e2e.sh** — `round_trip()`
  function had per-call `rm -f` only on the success path; failure
  paths leaked. Switched to script-level TMPDIR_E2E + trap; per-call
  rm dropped (the trap handles every return path including SIGINT).

Pattern: `mktemp -d -t prefix-XXX` for the dir, `mktemp <full-template>`
for files (portable across BSD/macOS + GNU coreutils — `-p` is
GNU-only and breaks Mac local-dev runs).

## Regression gate

New `tests/e2e/lint_cleanup_traps.sh` asserts every `*.sh` that calls
`mktemp` also has a `trap … EXIT` line in the file. Wired into the
existing Shellcheck (E2E scripts) CI step. Verified locally: passes
on the fixed state, fails-loud when one of the 3 fixes is reverted.

## Verification

  - shellcheck --severity=warning clean on all 4 touched files
  - lint_cleanup_traps.sh passes on the post-fix tree (6 mktemp users,
    all have EXIT trap)
  - Negative test: revert one fix → lint exits 1 with file:line +
    suggested fix pattern in the error message (CI-grokkable
    ::error file=… annotation)
  - Trap fires on SIGTERM mid-run (smoke-tested on macOS BSD mktemp)
  - Trap fires on `exit 1` (smoke-tested)

## Bars met (7-axis)

  - SSOT: trap pattern documented in lint message (one rule, one fix)
  - Cleanup: this IS the cleanup hygiene fix
  - 100% coverage: lint catches future regressions across all
    `tests/e2e/*.sh` files, not just the 3 fixed today
  - File-split: N/A (no files split)
  - Plugin / abstract / modular: N/A (test infra, not product code)

Iteration 2 of RFC #2873.
2026-05-05 04:21:26 -07:00

103 lines
5.0 KiB
Bash
Executable File
Raw Permalink 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
# E2E test: chat file attachment round-trip
#
# Proves the full drag-drop → agent-reads → agent-returns-file → download
# path against a live workspace. Runs against the local workspace-server
# on :8080 with a hermes workspace already online. The test is provider-
# agnostic as long as the agent has a valid API key — it only asserts
# that attachments surface on both ends, not a specific reply shape.
#
# Usage: WSID=<workspace-id> tests/e2e/test_chat_attachments_e2e.sh
# (pass WSID for an existing hermes workspace)
#
# Prereqs:
# - workspace-server on http://localhost:8080
# - the WSID workspace is online, runtime=hermes
# - a working provider key (MINIMAX_API_KEY / ANTHROPIC_API_KEY / etc.)
# - /workspace writable by the agent user (some templates ship it
# root-owned; chmod 777 for the E2E or use a writable template)
set -euo pipefail
WSID="${WSID:?WSID=<workspace-id> required}"
BASE="${BASE:-http://localhost:8080}"
# Per-run scratch dir collected under one trap so every mktemp leak path
# (assertion failure, SIGINT, exit non-zero) is plugged. Pre-fix this test
# created a /tmp/hermes-e2e-XXXXXX.txt and never deleted it — ~10 KB ×
# every CI run leaked into the runner. RFC #2873 cleanup-hygiene PR.
TMPDIR_E2E=$(mktemp -d -t chat-attachments-e2e-XXXXXX)
trap 'rm -rf "$TMPDIR_E2E"' EXIT INT TERM
log() { printf "\n=== %s ===\n" "$*"; }
log "Preflight: workspace online?"
STATUS=$(curl -s "$BASE/workspaces/$WSID" | python3 -c 'import json,sys;print(json.load(sys.stdin)["status"])')
[ "$STATUS" = "online" ] || { echo "workspace not online ($STATUS)"; exit 1; }
log "Step 1 — Upload a text file via /chat/uploads"
# `mktemp <full-template>` is portable across BSD (macOS) + GNU; -p is
# GNU-only and breaks local dev runs on Mac.
TEST_FILE=$(mktemp "$TMPDIR_E2E/hermes-e2e-XXXXXX.txt")
echo "secret code: $(openssl rand -hex 4)-$(openssl rand -hex 4)" > "$TEST_FILE"
EXPECTED=$(cat "$TEST_FILE" | awk '{print $NF}')
UPLOAD=$(curl -s -X POST "$BASE/workspaces/$WSID/chat/uploads" -F "files=@$TEST_FILE")
URI=$(echo "$UPLOAD" | python3 -c 'import json,sys;print(json.load(sys.stdin)["files"][0]["uri"])')
[ -n "$URI" ] || { echo "upload failed: $UPLOAD"; exit 1; }
echo "uploaded: $URI"
log "Step 2 — A2A message with file part; expect agent to quote the code"
# Build the JSON via a python helper so the URI value doesn't have to be
# shell-interpolated through a heredoc (the { } tokens in a JSON body
# collide with bash brace-expansion when quoted wrong).
PAYLOAD=$(URI="$URI" python3 -c '
import json, os
uri = os.environ["URI"]
print(json.dumps({
"jsonrpc":"2.0","id":"e2e-up","method":"message/send",
"params":{"message":{"role":"user","messageId":"e2e-up","kind":"message","parts":[
{"kind":"text","text":"Read the attached file and tell me the exact secret code."},
{"kind":"file","file":{"name":"test.txt","mimeType":"text/plain","uri":uri}},
]},"configuration":{"acceptedOutputModes":["text/plain"],"blocking":True}}}))
')
REPLY=$(curl -s -X POST "$BASE/workspaces/$WSID/a2a" \
-H 'Content-Type: application/json' \
--max-time 120 \
-d "$PAYLOAD")
REPLY_TEXT=$(echo "$REPLY" | python3 -c 'import json,sys;d=json.load(sys.stdin);[print(p.get("text","")) for p in d["result"]["parts"] if p.get("kind")=="text"]')
echo "agent reply: $REPLY_TEXT"
if echo "$REPLY_TEXT" | grep -qF "$EXPECTED"; then
echo "PASS: agent saw the attached file"
else
echo "FAIL: agent reply missing expected code '$EXPECTED'"
exit 1
fi
log "Step 3 — Seed a file inside /workspace and ask agent to reference it"
# Relies on /workspace being writable by the platform (we copy as root via
# docker exec, mimicking the path a real agent would use through its tools).
CONTAINER=$(docker ps --format '{{.Names}}' | grep -E "^ws-${WSID:0:12}" | head -1)
[ -n "$CONTAINER" ] || { echo "container not found"; exit 1; }
docker exec "$CONTAINER" sh -c 'echo "E2E report body $(date -u +%s)" > /workspace/e2e-report.txt'
REPLY=$(curl -s -X POST "$BASE/workspaces/$WSID/a2a" \
-H 'Content-Type: application/json' \
--max-time 120 \
-d '{"jsonrpc":"2.0","id":"e2e-down","method":"message/send","params":{"message":{"role":"user","messageId":"e2e-down","kind":"message","parts":[{"kind":"text","text":"There is a file at /workspace/e2e-report.txt. Mention its exact path in your reply so I can download it."}]},"configuration":{"acceptedOutputModes":["text/plain"],"blocking":true}}}')
FILE_URI=$(echo "$REPLY" | python3 -c 'import json,sys,re;d=json.load(sys.stdin);[print(p["file"]["uri"]) for p in d["result"]["parts"] if p.get("kind")=="file"]' | head -1)
[ -n "$FILE_URI" ] || { echo "FAIL: agent reply had no file part"; echo "$REPLY"; exit 1; }
echo "agent attached: $FILE_URI"
log "Step 4 — Download via /chat/download"
DL_PATH=${FILE_URI#workspace:}
BODY=$(curl -s "$BASE/workspaces/$WSID/chat/download?path=$DL_PATH")
echo "downloaded: $BODY"
if echo "$BODY" | grep -q "E2E report body"; then
echo "PASS: downloaded the agent-returned file"
else
echo "FAIL: download did not return expected body"
exit 1
fi
log "ALL E2E CHECKS PASSED"