Files
CST-agent/scripts/e2e_fixture.sh
WHC 2fde898da0 Usability pass: tool registry, TUI polish, saved keys and settings
Tools
- Table-driven tool registry with permission classes and default/minimal
  profiles; new tools: edit_file, list_dir, glob, grep, delete_file,
  move_file, todo_write, web_fetch, ask_user (plus read_file paging).
- Built-in Myers diff; write tools return unified diffs.
- Persistent permission rules (.agent/permissions.json), per-turn undo
  snapshots (.agent/undo), AGENTS.md project notes, auto-compact at 80%
  of the model context, cancellable run_shell with process-group kill.

TUI
- Table-driven slash commands: /provider /key /effort /status /tools
  /permissions /yolo /undo /retry /diff /export /copy /rename /delete
  /init /max-steps /todo, with / and @ completion.
- Permission modal with diff and prefix rules, ask_user modal, todo
  panel, header with model/effort/context, bracketed paste, line editing,
  wide-char cursor, NO_COLOR and ASCII fallback; Esc never quits.

Startup and configuration
- API keys are entered in the TUI (hidden) and saved per provider in
  <config>/credentials (0600); env variables are only a fallback.
- Last provider, per-provider model and reasoning effort are remembered
  in <config>/settings.json; --effort/-e and /effort send
  reasoning_effort (low|medium|high|max) to models that accept it.
- DeepSeek preset uses deepseek-flash / deepseek-v4-pro.
- Session flags --new/--continue/--resume/--list-sessions, --yolo,
  --confirm.

Compatibility and tests
- ncurses detection, macOS/Homebrew support, make install, XDG config
  dir, POSIX cleanups; Gitea CI (gcc/clang/asan/musl/macOS).
- Unit tests for diff, fs tools, registry, undo/perms and config;
  e2e fixtures multi_file_refactor and ask_or_todo; vendored cJSON.
2026-10-05 01:47:01 +08:00

149 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# One end-to-end run: copy fixtures/<name> to a temp workspace, run cst-agent
# headless on its TASK.md, then judge with the fixture's check.sh.
#
# Usage:
# scripts/e2e_fixture.sh <fixture> [--protocol P] [--provider NAME]
# [--model M] [--max-steps N] [--tools minimal|default] [--out DIR]
# [--record] [--baseline-only]
#
# --tools defaults to minimal (the four tools the paper experiments used).
#
# Artifacts go to <out>/<fixture>__<protocol>__<model>__<stamp>/:
# result.json agent.log session.jsonl metrics.jsonl check.log [sse/]
#
# Environment:
# CST_AGENT_BIN agent binary (default: <repo>/cst-agent, built if missing)
# API key CST_API_KEY, or the provider variable (DEEPSEEK_API_KEY,
# OPENAI_API_KEY, MINIMAX_API_KEY), or the credentials file
# E2E_TIMEOUT seconds before the agent is killed (default 900; needs
# `timeout` or `gtimeout` from coreutils)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FIXTURE=""
PROTOCOL="${CST_PROTOCOL:-native-eos}"
PROVIDER="${CST_PROVIDER:-}"
MODEL="${CST_MODEL:-}"
MAX_STEPS=""
TOOLS="${CST_E2E_TOOLS:-minimal}"
OUT="$ROOT/out/e2e"
RECORD=0
BASELINE_ONLY=0
while [[ $# -gt 0 ]]; do
case "$1" in
--protocol) PROTOCOL="$2"; shift 2 ;;
--provider) PROVIDER="$2"; shift 2 ;;
--model) MODEL="$2"; shift 2 ;;
--max-steps) MAX_STEPS="$2"; shift 2 ;;
--tools) TOOLS="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--record) RECORD=1; shift ;;
--baseline-only) BASELINE_ONLY=1; shift ;;
-h|--help) sed -n '2,21p' "$0"; exit 0 ;;
-*) echo "unknown option: $1" >&2; exit 64 ;;
*) FIXTURE="$1"; shift ;;
esac
done
FIX="$ROOT/fixtures/$FIXTURE"
if [[ -z "$FIXTURE" || ! -f "$FIX/TASK.md" || ! -x "$FIX/check.sh" ]]; then
echo "usage: $0 <fixture> [options] (fixture needs TASK.md + check.sh)" >&2
exit 64
fi
BIN="${CST_AGENT_BIN:-$ROOT/cst-agent}"
if [[ ! -x "$BIN" ]]; then
make -s -C "$ROOT" cst-agent >/dev/null || { echo "build failed" >&2; exit 65; }
fi
STAMP="$(date -u +%Y%m%dT%H%M%SZ)-$$"
MODEL_TAG="${MODEL:-default}"
RUN="$OUT/${FIXTURE}__${PROTOCOL}__${MODEL_TAG//\//_}__${STAMP}"
mkdir -p "$RUN"
WORK="$(mktemp -d /tmp/cst-e2e.XXXXXX)"
SCRATCH="$(mktemp -d /tmp/cst-e2e-base.XXXXXX)"
trap 'rm -rf "$WORK" "$SCRATCH"' EXIT
copy_fixture() {
cp -a "$FIX/." "$1/"
rm -rf "$1/out" "$1/.agent" "$1/.baseline_green_ok"
}
json_str() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }
write_result() {
local outcome="$1" agent_rc="$2" check_rc="$3" wall="$4"
cat >"$RUN/result.json" <<EOF
{"fixture": $(json_str "$FIXTURE"), "protocol": $(json_str "$PROTOCOL"),
"provider": $(json_str "${PROVIDER:-default}"), "model": $(json_str "$MODEL_TAG"),
"tools": $(json_str "$TOOLS"),
"run": $(json_str "$(basename "$RUN")"), "baseline_rc": $BASE_RC,
"agent_rc": $agent_rc, "check_rc": $check_rc, "wall_s": $wall,
"outcome": $(json_str "$outcome")}
EOF
echo "outcome=$outcome agent_rc=$agent_rc check_rc=$check_rc wall_s=$wall -> $RUN"
}
# Baseline in a throwaway copy: the task must not already be done.
copy_fixture "$SCRATCH"
"$SCRATCH/check.sh" >"$RUN/baseline.log" 2>&1
BASE_RC=$?
if [[ "$BASE_RC" -eq 0 && ! -e "$FIX/.baseline_green_ok" ]]; then
write_result unexpected_green_fixture 0 0 0
exit 2
fi
if [[ "$BASELINE_ONLY" -eq 1 ]]; then
write_result baseline_only 0 "$BASE_RC" 0
exit 0
fi
copy_fixture "$WORK"
args=(--headless --workspace "$WORK" --session "$WORK/.agent/e2e.jsonl"
--protocol "$PROTOCOL" --tools "$TOOLS")
[[ -n "$PROVIDER" ]] && args+=(--provider "$PROVIDER")
[[ -n "$MODEL" ]] && args+=(--model "$MODEL")
[[ -n "$MAX_STEPS" ]] && args+=(--max-steps "$MAX_STEPS")
mkdir -p "$WORK/.agent"
export CST_AUTO_APPROVE=1
export CST_METRICS_PATH="$RUN/metrics.jsonl"
if [[ "$RECORD" -eq 1 ]]; then
export CST_RECORD_SSE="$RUN/sse"
fi
now() { python3 -c 'import time; print(time.time())'; }
with_timeout() {
if command -v timeout >/dev/null 2>&1; then
timeout "${E2E_TIMEOUT:-900}" "$@"
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "${E2E_TIMEOUT:-900}" "$@"
else
"$@"
fi
}
t0=$(now)
with_timeout "$BIN" "${args[@]}" --prompt "$(cat "$FIX/TASK.md")" \
>"$RUN/agent.log" 2>&1
AGENT_RC=$?
t1=$(now)
WALL=$(python3 -c "print(round($t1-$t0,2))")
cp -f "$WORK/.agent/e2e.jsonl" "$RUN/session.jsonl" 2>/dev/null || true
(cd "$WORK" && ./check.sh) >"$RUN/check.log" 2>&1
CHECK_RC=$?
if [[ "$CHECK_RC" -eq 0 ]]; then
OUTCOME=pass
elif [[ "$AGENT_RC" -eq 124 ]]; then
OUTCOME=timeout
elif [[ "$AGENT_RC" -ne 0 ]]; then
OUTCOME=agent_error
else
OUTCOME=fail
fi
write_result "$OUTCOME" "$AGENT_RC" "$CHECK_RC" "$WALL"
[[ "$OUTCOME" == pass ]]