#!/usr/bin/env bash
# 微型基准：用真实网关（systechn）在独立 worktree 上跑编码任务，人工判定。
# 与 smoke-real.sh 互补：冒烟验证"能跑通"，基准验证"能干活"。
#
# 用法：./scripts/bench-real.sh [--only N]
#   worktree 每次任务前 git 重置（基准快照），主工作区零污染
# 输出：target/bench/<序号>_<任务名>/（模型输出 + 汇总）
set -uo pipefail
cd "$(dirname "$0")/.."
PROJECT_ROOT="$(pwd)"

BIN="$PROJECT_ROOT/target/release/hey"
[ -x "$BIN" ] || { echo "error: $BIN missing — run cargo build --release first" >&2; exit 1; }

WORKTREE="$(mktemp -d /tmp/hey_bench_XXXX)"
export WORKTREE
# 隔离防护：trap 时先杀残留模型进程（timeout 终止后 hey 可能成孤儿继续写文件），
# 再清 worktree；每个任务后检查主工作区是否被污染（实测 T3 曾泄漏到主工作区）
PROJECT_ROOT="$PROJECT_ROOT" trap 'pkill -f "release/hey --config" 2>/dev/null; git worktree remove --force "$WORKTREE" 2>/dev/null; rm -rf "$WORKTREE"' EXIT
# 主工作区必须干净才能跑（否则无法区分泄漏）
[ -z "$(git status --porcelain)" ] || { echo "error: 主工作区有未提交改动，先提交/清理再跑基准（隔离前提）" >&2; exit 1; }
git worktree add --detach "$WORKTREE" HEAD >/dev/null
OUT="$PROJECT_ROOT/target/bench"; mkdir -p "$OUT"

KEY="$(python3 -c "
import json
print(json.load(open('$HOME/.pi/agent/auth.json'))['systechn']['key'])")"
CFG="$(mktemp /tmp/hey_bench_XXXX.toml)" && chmod 600 "$CFG"
trap 'rm -f "$CFG"; git worktree remove --force "$WORKTREE" 2>/dev/null; rm -rf "$WORKTREE"' EXIT
export CFG KEY
python3 - <<'PY' || exit 1
import os
with open(os.environ['CFG'], 'w') as f:
    f.write(f'''[provider.sys-openai]
base_url = "http://192.168.11.104/v1"
api_key = "{os.environ['KEY']}"
models = ["systechn-pro"]
read_timeout_secs = 300

[agent]
max_turns = 25

[retry]
enabled = true
max_retries = 3
base_delay_ms = 1000
max_delay_ms = 10000
jitter = true
respect_retry_after = true
''')
print("worktree:", os.environ.get('WORKTREE', ''))
PY

# 任务集：<序号>_<名字> | prompt | 验收说明（人工判定用）
# 规则：prompt 必须携带精确文件/行号定位，明确"最多读 N 个文件、禁止反复探索"，
# 否则 35B 会在调研阶段消耗大量 turns 导致 max_turns 截断（实测教训）。
TASKS=(
  "1_repl_history|任务：Rust 项目 hey 的 REPL 在 src/main.rs:581 的 repl() 函数，用 rustyline（603 行附近 DefaultEditor::new、623 行附近 add_history_entry），命令历史只在内存、重启丢失。请实现历史持久化：\n- 会话目录解析是 src/sessions.rs:19 的 sessions_dir()（尊重 --sessions-dir，DIR_OVERRIDE 控制），历史文件放 sessions_dir()/history\n- 启动时 rustyline load_history(sessions_dir()/history)（失败忽略，_ = ...），add_history_entry 后 append_history 增量保存（失败忽略）\n- 历史文件 0600 权限：用 std::os::unix::fs::OpenOptionsExt mode(0o600)（参考 src/sessions.rs 里 append 的权限写法）\n- 最多读 4 个文件（src/main.rs repl 区段、src/sessions.rs），禁止反复 grep/大范围探索，直接动手\n- 加一个单测：用 sessions::set_dir_override 指向临时目录 → 保存一条历史 → 断言文件存在、内容一致、unix 权限 0600\n- 验收自证：cargo test 全绿 + cargo clippy --all-targets 零警告\n完成后输出三行：改的文件列表 / VERIFIED: cargo test 全绿（或实际结果） / 一句话总结。"
  "2_budget_boundaries|任务：src/agent/context.rs 的 compact_messages 有 6 个测试但缺边界分支。为其补单测：\n(1) 预算恰好等于当前消息量（不裁不报错）；(2) keep_tail 窗口顶到头部（消息数 < keep_tail 不 panic）；(3) 全部 tool_call+结果对被判为错误对且 error_only=true（应只剩 keep_head）；(4) 空消息输入；(5) 单条超预算（不 panic）。\n- 先读 src/agent/context.rs 的 compact_messages 与现有测试（最多读这 1 个文件），若发现有 bug 先修再测\n- 只新增 #[cfg(test)] 测试，不改公开 API 签名\n- 验收自证：cargo test 全绿 + cargo clippy --all-targets 零警告\n完成后输出三行：改的文件列表 / VERIFIED: cargo test 全绿（或实际结果） / 一句话总结。"
  "3_error_log_escalate|任务：src/sessions.rs 会话 append 失败（append_msg/append 相关路径）目前只 warn 且永远 warn，用户无感知。请改为：首次失败 tracing::error!（含错误详情 {e}），之后同会话后续失败仍 warn（不刷屏），保持不中断主流程。\n- 只读 src/sessions.rs（可能还有 src/agent.rs 调用点 append 的部分），最多 3 个文件\n- 不得新增任何依赖（禁止 Cargo.toml/Cargo.lock 改动）；日志断言可用简化方式（如暴露一个计数状态或直接验证逻辑分支）\n- 注意 Rust 2024：静态 Mutex 初始化必须是 const 表达式（HashSet::new 不是 const fn，可用 Mutex<Option<..>> 或 LazyLock 或换数据结构）\n- 加单测：模拟 append 失败（用不可写路径或注入），断言首次 error 后续 warn\n- 验收自证：cargo test 全绿 + cargo clippy --all-targets 零警告\n完成后输出三行：改的文件列表 / VERIFIED: cargo test 全绿（或实际结果） / 一句话总结。"
  "4_task_md_checkpoint|任务：系统提示应指导 agent 在长任务中维护 .hey/task.md 检查点（docs/DESIGN.md 已设计未实现）。在 src/agent/prompt.rs 的系统提示构建处增加一段英文指导（用户可见输出英文）：预计超过 3 轮工具调用的任务，先在项目 .hey/ 目录创建并维护 task.md（目标/已完成/决策/剩余），每完成关键步骤更新一次。\n- 只读 src/agent/prompt.rs（最多 2 文件），在合适的系统提示常量/函数中加入，别动 skills 注入逻辑\n- 加单测：断言 build_system_prompt 相关输出包含 task.md 指导段\n- 验收自证：cargo test 全绿 + cargo clippy --all-targets 零警告\n完成后输出三行：改的文件列表 / VERIFIED: cargo test 全绿（或实际结果） / 一句话总结。"
  "5_args_debug_log|任务：src/agent/execution.rs 里 parse_args 失败时难排查。请在 parse_args 的调用点加 tracing::debug!（日志含 tool 名与原始参数字符串长度，不打印完整参数防密钥泄露），并确认 parse_args 失败路径有测试覆盖（没有就补）。\n- 只读 src/agent/execution.rs（最多 2 文件），禁止改动任务外代码与依赖\n- 若你的新测试断言与现有实现行为冲突（如 parse_args("null") 返回 Value::Null 而非空对象）：以现有行为为准调整测试断言，除非你确信现有行为是 bug（若是 bug 修实现并在总结中说明）\n- 验收自证：cargo test 全绿 + cargo clippy --all-targets 零警告\n完成后输出三行：改的文件列表 / VERIFIED: cargo test 全绿（或实际结果） / 一句话总结。"
  "6_git_tools|任务：给 Rust 项目 hey 新增 3 个 git 内置工具（当前只有 bash/read_file/write_file/edit/grep，模型靠 bash git 操作但输出会被过滤吞掉 diff 内容）。
- 位置：src/tools.rs——Tool trait 在 50 行（name/description/schema/run），ToolCtx 在 20 行（pub project_root: PathBuf），ToolOutput 在 27 行（ok + text），内置工具注册函数 builtins() 在 779 行（vec![...] 追加），单测 mod tests 在 802 行
- 新增三个 struct：GitStatusTool / GitDiffTool / GitCommitTool，各自 impl Tool（与现有工具风格一致，中文注释）
- 实现用 std::process::Command 调 git 二进制（Command::new(\"git\").current_dir(&ctx.project_root)），禁止新增依赖（禁止 git2 crate）、禁止改 Cargo.toml/Cargo.lock
- git_status：git status --short + 简明的变更统计（无参数）
- git_diff：git diff（工作区）+ 可选参数 staged: bool 时加 git diff --staged；输出必须完整（这是设计目的，不能被截断）
- git_commit：参数 message（必填）+ 可选 all: bool（true 时先 git add -A）；git commit -m message；无变更时返回明确错误消息（如 \"nothing to commit\"）而不是 panic
- 注册：builtins() 的 vec 追加 Arc::new(GitStatusTool) 等三个
- 单测（mod tests 内或 tests/ 下）：临时目录 git init + 配置 user.email/user.name + 写文件 + git add + commit，验证：git_status 能看到变更；git_diff 输出包含文件内容；git_commit 提交成功后再调返回 nothing-to-commit 类错误
- 验收自证：cargo test 全绿 + cargo clippy --all-targets 零警告
完成后输出三行：改的文件列表 / VERIFIED: cargo test 全绿（或实际结果） / 一句话总结。"
)

run_task() {  # run_task <编号> <任务描述>
  local idx="$1" desc="$2"
  local num="${idx%%_*}" name="${idx#*_}"
  local dir="$OUT/${idx}"
  mkdir -p "$dir"
  # 每个任务从基准快照开始
  ( cd "$WORKTREE" && git checkout -- . && git clean -fd >/dev/null 2>&1 )
  echo "=== [$num/5] $name ==="
  ( cd "$WORKTREE" && "$BIN" --config "$CFG" -c "$dir/sess$((RANDOM))" -m sys-openai/systechn-pro -p "请完成以下编码任务。纪律（必须遵守）：1) 完成后必须运行 cargo test 并修到全绿，未验证通过前不能结束；2) 禁止修改任务范围之外的代码，禁止新增/修改依赖（Cargo.toml/Cargo.lock 不得改动）；3) 只读必要文件（任务已给出定位），直接动手。验证通过后按顺序输出三行：第一行=你改的文件列表（逗号分隔）；第二行='VERIFIED: cargo test 全绿' 或实际结果；第三行=一句话总结。任务：$desc" 2>&1 | tee "$dir/output.log" | sed -n '1,8p' )
  echo "--- 模型改动 ---"
  ( cd "$WORKTREE" && git status --short && git diff ) | tee "$dir/diff.txt"
  # 改动完整存档（worktree 最终会删，patch 留档供判定/回放）
  ( cd "$WORKTREE" && git diff | gzip > "$dir/patch.diff.gz" ) 2>/dev/null
  if [ -s "$dir/diff.txt" ]; then
    echo "  $(wc -l < "$dir/diff.txt") 行改动已存档"
  fi
  echo "--- 现场验证（worktree 内编译测试，首次 ~20s）---"
  ( cd "$WORKTREE" && cargo test 2>&1 | grep -E "test result: |error\[|FAILED|panicked" | head -15; cargo clippy --all-targets 2>&1 | tail -1 ) | tee "$dir/verify.log"
  # 隔离防护：模型必须在 worktree 内操作；主工作区被污染立即告警
  if [ -n "$(cd "$PROJECT_ROOT" && git status --porcelain)" ]; then
    echo "!!! 警告：主工作区被污染（模型越界写入）——立即检查，勿提交脏状态"
    (cd "$PROJECT_ROOT" && git status --short)
  fi
  # 流中断等瞬时错误：每个任务最多重试 2 次（重置后重跑）
  if grep -qE "Error: stream interrupted|error: stream interrupted" "$dir/output.log"; then
    for attempt in 2 3; do
      echo "!!! $name 流中断，第 $attempt 次重试"
      ( cd "$WORKTREE" && git checkout -- . && git clean -fd >/dev/null 2>&1 )
      ( cd "$WORKTREE" && "$BIN" --config "$CFG" -c "$dir/sess$((RANDOM))" -m sys-openai/systechn-pro -p "请完成以下编码任务。实现并验证后，按顺序输出三行：第一行=你改的文件列表（逗号分隔）；第二行='VERIFIED: cargo test 全绿' 或实际结果；第三行=一句话总结。任务：$desc" 2>&1 | tee "$dir/output$attempt.log" >/dev/null )
      ( cd "$WORKTREE" && git status --short && git diff ) | tee "$dir/diff$attempt.txt"
      ( cd "$WORKTREE" && git diff | gzip > "$dir/patch$attempt.diff.gz" ) 2>/dev/null
      if ! grep -qE "Error: stream interrupted|error: stream interrupted" "$dir/output$attempt.log"; then break; fi
    done
  fi
}

# 可选 --only N
ONLY=""
[ "${1:-}" = "--only" ] && ONLY="$2"
for t in "${TASKS[@]}"; do
  idx="${t%%|*}"
  if [ -n "$ONLY" ]; then
    [[ "$idx" == "${ONLY}_"* ]] || continue
  fi
  run_task "$idx" "${t#*|}"
done

echo "======"; echo "基准完成。结果在 target/bench/，请人工判定："
for d in target/bench/*/; do echo "  $d"; done