#!/usr/bin/env bash
# Copyright 2026 Mohsen Seyedkazemi Ardebili
# SPDX-License-Identifier: Apache-2.0
# Part of agent-fleet — https://github.com/MSKazemi/agent-fleet
# selftest — prove the fleet works, without touching a browser or a real site.
#
#   bin/selftest            run every check in a throwaway project
#   bin/selftest -v         show each command as it runs
#
# Covers everything that can be checked headlessly: project resolution, lane
# scheduling, the atomic claim race, the full task lifecycle, the note bridge,
# git hygiene, and tasks.json merging. Anything needing X11 or Chrome is skipped
# and reported as skipped — never silently passed.
set -uo pipefail

REPO="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
FLEET="${REPO}/bin/fleet"
VERBOSE="${1:-}"

PASS=0 FAIL=0 SKIP=0
TMP="$(mktemp -d "${TMPDIR:-/tmp}/fleet-selftest.XXXXXX")"
trap 'rm -rf "${TMP}"' EXIT

# The suite blocks tasks on purpose, and blocking raises a desktop notification.
# Running the tests must never put a fake "an agent needs your login" alert on
# a human's screen — the alert is only useful while it is always real.
export FLEET_NO_NOTIFY=1

ok()   { PASS=$((PASS+1)); printf '  \033[32m✓\033[0m %s\n' "$1"; }
bad()  { FAIL=$((FAIL+1)); printf '  \033[31m✗\033[0m %s\n' "$1"; [[ -n "${2:-}" ]] && printf '      %s\n' "$2"; }
skip() { SKIP=$((SKIP+1)); printf '  \033[33m-\033[0m %s (skipped: %s)\n' "$1" "$2"; }
group(){ printf '\n%s\n' "$1"; }

# check NAME EXPECTED ACTUAL
check() {
  [[ "$2" == "$3" ]] && ok "$1" || bad "$1" "expected [$2], got [$3]"
}

run() { [[ "${VERBOSE}" == "-v" ]] && echo "    \$ $*" >&2; "$@"; }

# --------------------------------------------------------------------------- #
group "project resolution"

P="${TMP}/alpha"; mkdir -p "${P}/src/deep"
(cd "${P}" && run "${FLEET}" init >/dev/null)
check "init creates the queue" "yes" "$([[ -d "${P}/.agents/queue/inbox" ]] && echo yes)"
check "resolves from a nested subdir" "${P}" \
      "$(cd "${P}/src/deep" && "${FLEET}" where | awk '/^root:/{print $2}')"

Q="${TMP}/beta"; mkdir -p "${Q}"
(cd "${Q}" && run "${FLEET}" init >/dev/null)
(cd "${P}" && "${FLEET}" add "https://a.example/1" --title A >/dev/null)
(cd "${Q}" && "${FLEET}" add "https://b.example/1" --title B >/dev/null)
check "two projects stay independent" "1" \
      "$(cd "${Q}" && "${FLEET}" plan --count)"

# --------------------------------------------------------------------------- #
group "lane scheduling"

W="${TMP}/lanes"; mkdir -p "${W}"; cd "${W}"; "${FLEET}" init >/dev/null
# These checks are about the lane rule, not about how busy the host is right now. Without this
# a loaded machine (another job, a build) correctly plans one worker and fails the suite.
python3 - "${W}/.agents/config.json" <<'PY'
import json, sys
p = sys.argv[1]; c = json.load(open(p)); c["max_load_per_core"] = 1000
json.dump(c, open(p, "w"), indent=2)
PY
"${FLEET}" add "https://site-one.example/a" --title one   >/dev/null
"${FLEET}" add "https://site-one.example/b" --title two   >/dev/null
"${FLEET}" add "https://site-two.example/c" --title three >/dev/null
"${FLEET}" add "write a summary of the three roles" --kind research >/dev/null
"${FLEET}" add "update the CV master file" --serial >/dev/null

PLAN="$("${FLEET}" plan --json)"
lane() { python3 -c "import json,sys;print(json.loads(sys.argv[1])['lanes'][sys.argv[2]])" "${PLAN}" "$1"; }
check "URLs infer the browser lane"      "3" "$(lane browser)"
check "plain text infers the agent lane" "1" "$(lane agent)"
check "--serial forces the serial lane"  "1" "$(lane serial)"
check "terminals = one per site, not per task" "2" "$("${FLEET}" plan --count)"

"${FLEET}" claim w1 --exec agent >/dev/null
check "lane-filtered claim leaves browser work alone" "2" "$("${FLEET}" plan --count)"

# --------------------------------------------------------------------------- #
group "machine awareness"

M="${TMP}/machine"; mkdir -p "${M}"; cd "${M}"; "${FLEET}" init >/dev/null
for i in $(seq 1 40); do
  "${FLEET}" add "https://host${i}.example/x" --title "t${i}" >/dev/null
done
setcfg() { python3 - "${M}/.agents/config.json" "$@" <<'PYCFG'
import json, sys
path, pairs = sys.argv[1], sys.argv[2:]
cfg = json.load(open(path))
for pair in pairs:
    k, _, v = pair.partition("=")
    cfg[k] = None if v == "null" else (int(v) if v.lstrip("-").isdigit() else v)
json.dump(cfg, open(path, "w"), indent=2)
PYCFG
}
mach() { python3 -c 'import json,sys;print(json.loads(sys.argv[1])["machine"][sys.argv[2]])' \
         "$("${FLEET}" plan --json)" "$1"; }

check "the queue length never sets the worker count" "1" \
      "$([[ "$("${FLEET}" plan --count)" -lt 40 ]] && echo 1)"

# nothing about the machine may be a fixed assumption
MACH="$("${FLEET}" machine --json)"
mval() { python3 -c 'import json,sys;print(json.loads(sys.argv[1])[sys.argv[2]])' "${MACH}" "$1"; }
mprov() { python3 -c 'import json,sys;print(json.loads(sys.argv[1])["provenance"][sys.argv[2]])' "${MACH}" "$1"; }

check "memory reserve scales with the machine, not a constant" "1" \
      "$(python3 -c "
import json,sys
m = json.loads(sys.argv[1])
want = max(1024, int(m['total_mb'] * 0.10))
print(1 if m['reserve_mem_mb'] == want else 0)" "${MACH}")"
check "core reserve scales with core count" "1" \
      "$(python3 -c "
import json,sys
m = json.loads(sys.argv[1])
print(1 if m['reserve_cores'] == max(1, m['cores'] // 8) else 0)" "${MACH}")"
check "reserve is derived, not hard-coded"   "derived from total RAM" "$(mprov reserve_mem_mb)"
check "worker cost is measured or fallback"  "1" \
      "$([[ "$(mprov mem_per_browser_worker_mb)" =~ ^(measured|fallback|remembered)$ ]] && echo 1)"
check "worker cost is a real number"         "1" \
      "$([[ "$(mval mem_per_browser_worker_mb)" -gt 50 ]] && echo 1)"
check "the estimate is remembered per project" "1" \
      "$([[ -f "${M}/.agents/machine.json" ]] && echo 1)"

# an explicit setting still wins over measurement
setcfg mem_per_browser_worker_mb=4242
check "a pinned cost overrides measurement" "configured" \
      "$(python3 -c 'import json,sys;print(json.loads(sys.argv[1])["provenance"]["mem_per_browser_worker_mb"])' \
         "$("${FLEET}" machine --json)")"
setcfg mem_per_browser_worker_mb=null

# a machine with no spare memory must fall back to serial, not refuse or overload
setcfg max_workers=50 mem_per_browser_worker_mb=2000000 reserve_mem_mb=0
check "no memory → exactly one worker, not zero" "1" "$("${FLEET}" plan --count)"
check "and it says so"                           "1" \
      "$("${FLEET}" plan | grep -c 'one at a time')"

# a roomy machine may go wide, but never past what it can spare
setcfg mem_per_browser_worker_mb=1 reserve_mem_mb=0 reserve_cores=0
CAP="$(mach capacity)"
check "capacity is reported"        "1" "$([[ "${CAP}" -ge 1 ]] && echo 1)"
check "workers never exceed capacity" "1" \
      "$([[ "$("${FLEET}" plan --count)" -le "${CAP}" ]] && echo 1)"

# --force is the only way past the machine ceiling
setcfg mem_per_browser_worker_mb=2000000 reserve_mem_mb=0 max_workers=6
check "--force overrides the machine ceiling" "6" "$("${FLEET}" plan --count --force)"
check "without --force it stays safe"         "1" "$("${FLEET}" plan --count)"

# opting out of machine awareness is possible but explicit
setcfg respect_machine=0
check "respect_machine=false trusts max_workers" "6" "$("${FLEET}" plan --count)"
setcfg respect_machine=1

# a container memory limit must be honoured when present
check "container limits are read when present" "1" \
      "$(python3 -c "
import json,sys
p = json.loads(sys.argv[1])
print(1 if 'containerised' in p['machine'] else 0)" "$("${FLEET}" plan --json)")"

# --------------------------------------------------------------------------- #
group "launching without a GUI"

H="${TMP}/headless"; mkdir -p "${H}"; cd "${H}"; "${FLEET}" init >/dev/null
"${FLEET}" add "https://headless.example/1" --title h1 >/dev/null
DRY="$("${FLEET}" run --headless --dry 2>&1)"
check "headless --dry starts nothing"   "1" "$(grep -c 'would start 1 headless' <<<"${DRY}")"
check "worker carries its own AGENT_ID" "1" "$(grep -c 'AGENT_ID=agent-1' <<<"${DRY}")"

"${FLEET}" launch 2 --headless --cmd "sleep 3" >/dev/null 2>&1
check "headless launch writes a log per worker" "2" \
      "$(ls .agents/logs/agent-*.log 2>/dev/null | grep -c .)"

# a kind whose skills live elsewhere must start there, queue unchanged
ELSEWHERE="${TMP}/toolbox"; mkdir -p "${ELSEWHERE}"
python3 - "${H}/.agents/config.json" "${ELSEWHERE}" <<'PYCFG'
import json, sys
path, elsewhere = sys.argv[1], sys.argv[2]
cfg = json.load(open(path))
cfg["workers_start_in_by_kind"] = {"special": elsewhere}
json.dump(cfg, open(path, "w"), indent=2)
PYCFG
"${FLEET}" add "https://special.example/1" --kind special --title s1 >/dev/null
CWD_FOR_KIND="$(python3 -c 'import json,sys;print(json.loads(sys.argv[1])["worker_cwd"])' \
                "$("${FLEET}" plan --json)")"
check "workers follow their kind's skills" "${ELSEWHERE}" "${CWD_FOR_KIND}"

python3 - "${H}/.agents/config.json" <<'PYCFG'
import json, sys
cfg = json.load(open(sys.argv[1]))
cfg["workers_start_in_by_kind"] = {"special": "/nonexistent/path"}
json.dump(cfg, open(sys.argv[1], "w"), indent=2)
PYCFG
check "doctor catches a missing workers_start_in" "1" \
      "$("${FLEET}" doctor >/dev/null 2>&1; echo $?)"
python3 - "${H}/.agents/config.json" <<'PYCFG'
import json, sys
cfg = json.load(open(sys.argv[1]))
cfg["workers_start_in_by_kind"] = {}
json.dump(cfg, open(sys.argv[1], "w"), indent=2)
PYCFG
check "doctor accepts a solvable scope" "0" "$("${FLEET}" doctor >/dev/null 2>&1; echo $?)"

# --------------------------------------------------------------------------- #
group "atomic claim"

R="${TMP}/race"; mkdir -p "${R}"; cd "${R}"; "${FLEET}" init >/dev/null
for i in 1 2 3 4; do "${FLEET}" add "https://race.example/${i}" --title "r${i}" >/dev/null; done
for i in $(seq 1 8); do
  ("${FLEET}" claim "racer-${i}" 2>/dev/null | python3 -c \
     "import sys,json;d=sys.stdin.read().strip();print(json.loads(d)['id']) if d else None") &
done >"${TMP}/winners.txt"
wait
WON=$(grep -c . "${TMP}/winners.txt" || true)
UNIQ=$(sort -u "${TMP}/winners.txt" | grep -c . || true)
check "8 agents, 4 tasks → 4 winners"   "4" "${WON}"
check "no task claimed twice"           "${WON}" "${UNIQ}"

# --------------------------------------------------------------------------- #
group "task lifecycle"

L="${TMP}/life"; mkdir -p "${L}"; cd "${L}"; "${FLEET}" init >/dev/null
"${FLEET}" add "https://life.example/1" --title keep >/dev/null
ID="$("${FLEET}" claim solo | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')"
"${FLEET}" block "${ID}" --reason "login required: example" >/dev/null
check "block parks the task"       "1" "$("${FLEET}" list blocked | grep -c "${ID}")"
"${FLEET}" unblock --all >/dev/null
check "unblock returns it"         "1" "$("${FLEET}" plan --count)"
check "unblock with no id refuses" "2" "$("${FLEET}" unblock >/dev/null 2>&1; echo $?)"
# -- the notification a blocked task raises ---------------------------------

# Prove the suppression rather than trusting it: a stub notify-send on PATH
# records every call, so both directions are observable.
FAKEBIN="${TMP}/fakebin"; mkdir -p "${FAKEBIN}"
NOTIFY_LOG="${TMP}/notify.log"; : > "${NOTIFY_LOG}"
# Args are: --app-name=... --urgency=... TITLE BODY, so the title is \$3. Log
# the title only: the body is deliberately two lines, and counting lines instead
# of calls would silently turn one alert into two.
cat > "${FAKEBIN}/notify-send" <<EOS
#!/usr/bin/env bash
printf 'CALL %s\n' "\$3" >> "${NOTIFY_LOG}"
EOS
chmod +x "${FAKEBIN}/notify-send"
NID="$("${FLEET}" claim solo | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')"
PATH="${FAKEBIN}:${PATH}" "${FLEET}" block "${NID}" --reason "quiet" >/dev/null
check "FLEET_NO_NOTIFY silences the alert" "0" "$(wc -l < "${NOTIFY_LOG}")"
"${FLEET}" unblock --all >/dev/null
NID="$("${FLEET}" claim solo | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')"
PATH="${FAKEBIN}:${PATH}" env -u FLEET_NO_NOTIFY "${FLEET}" block "${NID}" --reason "loud" >/dev/null
check "without it, blocking still alerts" "1" "$(wc -l < "${NOTIFY_LOG}")"
check "the alert names the reason"        "1" "$(grep -c '^CALL Agent blocked: loud$' "${NOTIFY_LOG}")"
"${FLEET}" unblock --all >/dev/null

"${FLEET}" claim solo >/dev/null
"${FLEET}" reset --older-than 0 >/dev/null
check "reset recovers a dead agent's task" "1" "$("${FLEET}" plan --count)"
ID="$("${FLEET}" claim solo | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')"
"${FLEET}" done "${ID}" --result "confirmation #1" >/dev/null
check "done empties the queue"     "0" "$("${FLEET}" plan --count)"
check "claim on empty exits 3"     "3" "$("${FLEET}" claim solo >/dev/null 2>&1; echo $?)"

# --------------------------------------------------------------------------- #
group "git hygiene"

G="${TMP}/repo"; mkdir -p "${G}/.vscode"; cd "${G}"
git init -q .
printf 'node_modules/\n' > .gitignore
cat > .vscode/tasks.json <<'JSON'
{
  "version": "2.0.0",
  // the project's own build task
  "tasks": [
    { "label": "build", "type": "shell", "command": "make all",
      "group": { "kind": "build", "isDefault": true }, }
  ]
}
JSON
printf 'x\n' > src.txt
git add -A >/dev/null
git -c user.email=t@e -c user.name=t -c commit.gpgsign=false commit -qm init --no-verify >/dev/null 2>&1
BEFORE_IGNORE="$(cat .gitignore)"
"${FLEET}" init >/dev/null
# The starter checklist is the ONLY thing init adds to a repo, and it is left
# visible on purpose: it is the user's own content, so excluding it would keep
# their backlog out of their own repo. Fleet's machine-local artifacts must
# still never appear — that is what this pair pins down.
check "init adds only the checklist"    "?? TASKS.md" "$(git status --porcelain)"
check "the checklist is theirs to commit" "1" "$(git check-ignore -q TASKS.md; echo $?)"
check "init does not touch .gitignore"  "${BEFORE_IGNORE}" "$(cat .gitignore)"
check ".agents/ is excluded"            "0" "$(git check-ignore -q .agents; echo $?)"

"${FLEET}" add "https://g.example/1" --title g >/dev/null
"${FLEET}" run --vscode --dry >/dev/null 2>&1   # force the tasks.json path
LABELS="$(python3 -c "
import json;print(','.join(t['label'] for t in json.load(open('.vscode/tasks.json'))['tasks']))")"
check "merge keeps the project's task"  "build,agent-1,agents: all" "${LABELS}"
check "our compound task is the default" "True" "$(python3 -c "
import json
t=[x for x in json.load(open('.vscode/tasks.json'))['tasks'] if x['label']=='agents: all'][0]
print(t['group']['isDefault'])")"
check "the project's default was demoted" "False" "$(python3 -c "
import json
t=[x for x in json.load(open('.vscode/tasks.json'))['tasks'] if x['label']=='build'][0]
print(t['group']['isDefault'])")"
"${FLEET}" clean >/dev/null
check "clean restores the original"     "?? TASKS.md" "$(git status --porcelain)"

# a queue that leaked into git must be caught
mkdir -p .agents/queue/inbox && echo '{}' > .agents/queue/inbox/leak.json
git add -f .agents >/dev/null 2>&1
check "doctor detects a tracked queue"  "1" "$("${FLEET}" doctor >/dev/null 2>&1; echo $?)"
"${FLEET}" doctor --fix >/dev/null 2>&1
check "doctor --fix untracks it"        "0" "$(git ls-files -- .agents | grep -c . || true)"

# --------------------------------------------------------------------------- #
group "browser reality check"

# a git repo, so doctor runs its whole path and can offer (or withhold) --fix
B="${TMP}/browser"; mkdir -p "${B}"; cd "${B}"; git init -q .
"${FLEET}" init >/dev/null
"${FLEET}" add "https://tab.example/1" --title needs-a-tab >/dev/null
check "no probe means unknown, never assumed" "1" \
      "$("${FLEET}" doctor 2>/dev/null | grep -c 'is unknown')"
check "probing never happens by accident"     "0" \
      "$([[ -f .agents/browser-probe.json ]] && echo 1 || echo 0)"
check "--dry names the worker command, asks nothing" "1" \
      "$("${FLEET}" probe-browser --dry | grep -c 'claude --dangerously-skip-permissions')"

probe() { python3 - "${B}/.agents/browser-probe.json" "$1" "${2:-print}" <<'PYPROBE'
import json, sys
json.dump({"checked_at": "2026-01-01T00:00:00+00:00",
           "browser": sys.argv[2] == "yes", "mode": sys.argv[3]},
          open(sys.argv[1], "w"))
PYPROBE
}

# The default probe asks with -p, so it measures PRINT mode. A terminal worker is
# the same binary run interactively: it is offered the Chrome toolset and gets its
# own per-session tab group, and two sessions can hold live connections to one
# Chrome at once. Reporting a print-mode NO as "the browser lane is broken" talks
# people out of the one arrangement that works, so the verdict is scoped by mode.
probe no print
check "a print-mode NO is not a verdict on terminals" "0" \
      "$("${FLEET}" doctor >/dev/null 2>&1; echo $?)"
check "doctor says what the NO does not cover"   "1" \
      "$("${FLEET}" doctor 2>/dev/null | grep -c 'terminal workers are not')"
check "a headless run is still warned"           "1" \
      "$("${FLEET}" run --headless --dry 2>&1 | grep -c 'found no browser')"

probe no terminal
check "a browserless worker is a doctor problem" "1" \
      "$("${FLEET}" doctor >/dev/null 2>&1; echo $?)"
check "and --fix is not offered for it"          "0" \
      "$("${FLEET}" doctor 2>/dev/null | grep -c 'doctor --fix')"
check "run warns before starting doomed workers" "1" \
      "$("${FLEET}" run --headless --dry 2>&1 | grep -c 'found no browser')"

probe yes
check "a working browser clears the problem"     "0" \
      "$("${FLEET}" doctor >/dev/null 2>&1; echo $?)"

# --------------------------------------------------------------------------- #
group "checklist bridge"

# -- the starter checklist init leaves behind --------------------------------
# Its examples are indented on purpose, so a brand-new project starts with an
# empty queue: a first `from-note` that invented six tasks would be a trap.
S="${TMP}/starter"; mkdir -p "${S}"; cd "${S}"; "${FLEET}" init >/dev/null
check "init creates a starter checklist" "yes" "$([[ -f TASKS.md ]] && echo yes || echo no)"
"${FLEET}" from-note >/dev/null 2>&1
check "the starter queues nothing"       "0" \
      "$("${FLEET}" list inbox | grep -cE '^  [0-9a-f]{8}')"

S2="${TMP}/starter-off"; mkdir -p "${S2}"; cd "${S2}"
"${FLEET}" init --no-checklist >/dev/null
check "--no-checklist skips it"          "no" "$([[ -f TASKS.md ]] && echo yes || echo no)"

# A checklist the project already keeps is theirs. Never add a second one, and
# never rewrite the first.
S3="${TMP}/starter-keep"; mkdir -p "${S3}"; cd "${S3}"
printf '## TODO\n\n- [ ] mine\n' > TODO.md
"${FLEET}" init >/dev/null
check "an existing checklist is honoured" "no" "$([[ -f TASKS.md ]] && echo yes || echo no)"
check "and is left byte-for-byte alone"   "- [ ] mine" "$(grep -m1 mine TODO.md)"

# -- promotion ---------------------------------------------------------------
N="${TMP}/checklist"; mkdir -p "${N}"; cd "${N}"; "${FLEET}" init >/dev/null
cat > TASKS.md <<'MD'
# Backlog

## READY

- [ ] `N1` · apply at https://note.example/form
  - why: it is the first real one
  - [ ] sub-step that must not become its own task
- [ ] ! `N2` · draft the summary
- [ ] `N3` · ⇒ · update the changelog
- [ ] ? `N4` · which option should I take
- [ ] 🔒 `N5` · hands off
- [x] `N6` · already done
- [ ] `N7` · rewrite the CLI reference
  - touches: docs/cli.md
- [ ] `N8` · the DRAFT is ready and 🔒 nothing is posted yet!!
  - touches: docs/plan.md
MD
OUT="$("${FLEET}" from-note)"
check "checklist items become tasks"   "5" "$(grep -cE '^[0-9a-f]{8} ' <<<"${OUT}")"
check "? and 🔒 are never queued"      "1" "$(grep -c 'skipped 2' <<<"${OUT}")"
check "! raises priority"              "1" "$("${FLEET}" list inbox | grep -c 'p2 ')"
check "⇒ forces the serial lane"       "1" "$(grep -cE '^[0-9a-f]{8} +serial +p[0-9]+ +.*changelog' <<<"${OUT}")"

# ⛔ The parallel lane is OPT-IN. This used to return `agent` — the CONCURRENT lane — for
# everything not ⇒-marked and without a URL, so two items writing the same file were handed to
# two workers at once, and nothing recorded which files an item writes so nothing could tell.
check "undeclared work runs serial"    "1" "$(grep -cE '^[0-9a-f]{8} +serial +p[0-9]+ +.*summary' <<<"${OUT}")"
check "touches: earns the agent lane"  "1" "$(grep -cE '^[0-9a-f]{8} +agent +p[0-9]+ +.*CLI reference' <<<"${OUT}")"

# ⛔ Markers count only in the prefix. `"🔒" in body` silently dropped a real kubeintellect item
# whose title is followed by the prose "🔒 the DRAFT is ready"; `"!!" in body` invented a
# priority from an exclamation mark in a sentence.
check "prose 🔒 does not drop an item"  "1" "$(grep -cE '^[0-9a-f]{8} +agent +p[0-9]+ +.*DRAFT is ready' <<<"${OUT}")"
check "prose !! does not set priority" "0" "$("${FLEET}" list inbox | grep -c 'p1 ')"
check "sub-steps stay with their task" "0" "$("${FLEET}" list inbox | grep -c 'sub-step')"
check "re-running does not duplicate"  "5" \
      "$("${FLEET}" from-note >/dev/null; "${FLEET}" list inbox | grep -cE '^  [0-9a-f]{8}')"

cp TASKS.md "${TMP}/checklist.before"
NID="$("${FLEET}" list inbox | awk '/N1/{print $1}')"
"${FLEET}" done "${NID}" --result "submitted; confirmation #9" >/dev/null
"${FLEET}" to-note >/dev/null
check "write-back ticks exactly one line" "1" \
      "$(diff "${TMP}/checklist.before" TASKS.md | grep -c '^> - \[x\]')"
check "write-back is idempotent"       "nothing to write back" "$("${FLEET}" to-note)"

# --------------------------------------------------------------------------- #
group "environment (needed only to open terminals)"

[[ "${XDG_SESSION_TYPE:-}" == "x11" ]] && ok "X11 session" \
  || skip "X11 session" "XDG_SESSION_TYPE=${XDG_SESSION_TYPE:-unset}; fleet run/open cannot work"
command -v xdotool >/dev/null && ok "xdotool installed" \
  || skip "xdotool" "install it to open terminals"
command -v notify-send >/dev/null && ok "notify-send installed" \
  || skip "notify-send" "blocked tasks will not raise a desktop notification"
command -v claude >/dev/null && ok "claude on PATH" \
  || skip "claude CLI" "workers cannot start without it"

# --------------------------------------------------------------------------- #
group "colliding work is never handed to two workers"

# ⛔ THE FAILURE THIS WHOLE CHANGE EXISTS TO PREVENT. Two agent-lane items writing the SAME
# file used to be claimable at once: `classify` put everything undeclared in the concurrent
# lane, and nothing recorded which files a task writes, so nothing could tell.
#
# Asserted as an INVARIANT over whatever ends up in flight, not as a claim-by-claim script:
# the queue carries tasks from earlier groups, and a test that assumed an empty inbox would
# pass or fail on ordering rather than on the property.
cat > COLLIDE.md <<'MD'
## READY

- [ ] `C1` · first writer
  - touches: shared/thing.py
- [ ] `C2` · second writer, same file
  - touches: shared/thing.py
- [ ] `C3` · unrelated writer
  - touches: docs/other.md
MD
"${FLEET}" from-note --note COLLIDE.md >/dev/null 2>&1
for i in 1 2 3 4 5 6 7 8 9 10; do "${FLEET}" claim "w${i}" --exec agent >/dev/null 2>&1 || true; done
INFLIGHT="$(find . -path '*/queue/active/*.json' -exec cat {} + 2>/dev/null \
            | grep -c '"shared/thing.py"' || true)"
check "only one writer of a file is live" "1" "${INFLIGHT}"
check "the other is left in the inbox"    "1" \
      "$(find . -path '*/queue/inbox/*.json' -exec cat {} + 2>/dev/null | grep -c '"shared/thing.py"' || true)"

group "terminal windows without X11"

# Most desktops are Wayland, where the VS Code/xdotool path cannot work and the old fallback was
# headless — a session with no Chrome tools, so every browser task came back blocked. A stub
# emulator stands in for the real one: the point is WHAT would be opened, not drawing a window.
WN="${TMP}/windows"; mkdir -p "${WN}/bin"; cd "${WN}"; "${FLEET}" init >/dev/null
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$@" > "%s/call-$$.txt"\n' "${WN}" > "${WN}/bin/gnome-terminal"
chmod +x "${WN}/bin/gnome-terminal"
for u in a b c; do "${FLEET}" add "https://${u}.example/1" --title "${u}" >/dev/null; done
WENV=(env PATH="${WN}/bin:${PATH}" XDG_SESSION_TYPE=wayland WAYLAND_DISPLAY=wayland-0 FLEET_TERMINAL=gnome-terminal)
"${WENV[@]}" "${FLEET}" run 3 --stagger 0 --force >"${WN}/out.txt" 2>&1; sleep 1
check "no X11 + a display opens windows"   "1" "$(grep -c 'opening terminal windows instead' "${WN}/out.txt")"
check "one window per worker"              "3" "$(ls "${WN}"/call-*.txt 2>/dev/null | grep -c .)"
check "each worker is started with --chrome" "3" "$(cat "${WN}"/call-*.txt | grep -c 'claude --chrome ')"
check "workers get distinct ids"           "3" "$(cat "${WN}"/call-*.txt | grep -o 'AGENT_ID=agent-[0-9]' | sort -u | grep -c .)"
check "a dead worker leaves its window up" "3" "$(cat "${WN}"/call-*.txt | grep -c 'press Enter to close')"
check "--headless still means no window"   "0" \
      "$("${WENV[@]}" "${FLEET}" run --headless --dry 2>&1 | grep -c 'terminal window')"

group "human in the loop"

# The wall a worker cannot pass is a person's to clear. These pin the loop: park with a reason,
# the human answers or logs in, the task returns carrying the answer — and clearing a wall does
# not count against the task.
HL="${TMP}/human"; mkdir -p "${HL}"; cd "${HL}"; "${FLEET}" init >/dev/null
"${FLEET}" add "https://h.example/form" --title form --note "apply with the standard CV" >/dev/null
HID="$("${FLEET}" claim agent-9 | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')"
"${FLEET}" block "${HID}" --reason "needs answer: expected salary?" >/dev/null
check "blocked list names the worker"    "1" "$("${FLEET}" blocked | grep -c 'was agent-9')"
check "and says how to answer"           "1" "$("${FLEET}" blocked | grep -c 'unblock .* --answer')"
check "--answer refuses --all"           "2" "$("${FLEET}" unblock --all --answer x >/dev/null 2>&1; echo $?)"
"${FLEET}" unblock "${HID}" --answer "no figure; say negotiable" >/dev/null
check "the answer reaches the next worker" "1" \
      "$("${FLEET}" show "${HID}" | grep -c 'human answer .* no figure; say negotiable')"
check "the original brief survives"      "1" "$("${FLEET}" show "${HID}" | grep -c 'standard CV')"
check "clearing a wall is not an attempt" "0" \
      "$("${FLEET}" show "${HID}" | python3 -c 'import sys,json;print(json.load(sys.stdin)["attempts"])')"
check "interactive prompt is opt-in"     "0" \
      "$(env PATH="${WN}/bin:${PATH}" XDG_SESSION_TYPE=wayland WAYLAND_DISPLAY=w FLEET_TERMINAL=gnome-terminal "${FLEET}" run 1 --windows --dry 2>&1 | grep -c 'This run is interactive')"
check "--ask puts the worker in the loop" "1" \
      "$(env PATH="${WN}/bin:${PATH}" XDG_SESSION_TYPE=wayland WAYLAND_DISPLAY=w FLEET_TERMINAL=gnome-terminal "${FLEET}" run 1 --windows --ask --dry 2>&1 | grep -c 'This run is interactive')"

# A worker started with --chrome must still be counted: status and the VS Code launch check both
# find workers by their command line, and adding a flag once made every worker invisible.
CNT="${TMP}/count"; mkdir -p "${CNT}"; cd "${CNT}"; "${FLEET}" init >/dev/null
AGENT_ROOT="${CNT}" bash -c 'exec -a "claude --chrome --dangerously-skip-permissions" sleep 15' &
CNT_PID=$!; sleep 1
check "a --chrome worker is counted" "1" "$("${FLEET}" status | awk '/^agents:/{print $2}')"
kill "${CNT_PID}" 2>/dev/null || true      # by pid: never by pattern, which would also hit real workers

group "a renamed queue directory"

# A project may keep its queue under another name and below the repo root (AGENT_DIR +
# AGENT_ROOT). The engine and the launcher must agree about where it is, the ignore rule
# must hide that directory and nothing else, and tasks.json belongs to the folder VS Code
# opens (FLEET_WORKSPACE) — not to the folder the queue happens to sit in.
RN="${TMP}/renamed"; mkdir -p "${RN}/data" "${RN}/other/fleet"; (cd "${RN}" && git init -q .)
export AGENT_ROOT="${RN}/data" AGENT_DIR=fleet
(cd "${RN}" && "${FLEET}" init >/dev/null)
check "init builds the renamed queue"   "yes" "$([[ -d "${RN}/data/fleet/queue/inbox" ]] && echo yes)"
check "and no phantom .agents beside it" "no" "$([[ -e "${RN}/data/.agents" ]] && echo yes || echo no)"
(cd "${RN}" && "${FLEET}" add "https://r.example/1" --title r >/dev/null)
check "add writes into that same queue" "1" \
      "$(find "${RN}/data/fleet/queue/inbox" -name '*.json' | grep -c .)"
check "the renamed queue is ignored"    "0" "$(cd "${RN}" && git check-ignore -q data/fleet; echo $?)"
check "a same-named source dir is not"  "1" "$(cd "${RN}" && git check-ignore -q other/fleet; echo $?)"
(cd "${RN}" && FLEET_WORKSPACE="${RN}" "${FLEET}" run --vscode --dry >/dev/null 2>&1) || true
check "tasks.json goes to the workspace" "yes" \
      "$([[ -f "${RN}/.vscode/tasks.json" && ! -e "${RN}/data/.vscode" ]] && echo yes)"
unset AGENT_ROOT AGENT_DIR

check "the worker protocol forbids a second submit" "1" \
      "$(grep -c 'Submit exactly once' "${REPO}/skills/agent-fleet/references/worker-protocol.md")"

group "release metadata agrees with VERSION"

# One version, written once. Every other place that states it is checked here, so a bump that
# misses CITATION.cff or a plugin manifest fails the suite instead of shipping a lie.
VER="$(tr -d '[:space:]' < "${REPO}/VERSION")"
check "fleet --version reports VERSION"      "agent-fleet ${VER}" "$("${FLEET}" --version)"
check "CITATION.cff states VERSION"          "${VER}" \
      "$(sed -n 's/^version: *//p' "${REPO}/CITATION.cff")"
check "the plugin manifests state VERSION"   "2" \
      "$(grep -c "\"version\": \"${VER}\"" "${REPO}/.claude-plugin/plugin.json" "${REPO}/.claude-plugin/marketplace.json" | grep -c ':1$')"
check "pyproject reads its version from VERSION" "1" \
      "$(grep -c -E '^dynamic = \["version"\]' "${REPO}/pyproject.toml")"

group "documentation matches reality"

# The count below includes this check itself, so adding a check makes this fail
# until the docs are updated — which is the point. Docs that quote a number the
# suite no longer produces are worse than docs quoting none.
TOTAL=$((PASS + FAIL + SKIP + 1))
DRIFT=""
while read -r hit; do
  [[ -z "${hit}" ]] && continue
  file="${hit%%:*}"; num="$(grep -oE '^[0-9]+' <<<"${hit##*:}")"
  [[ "${num}" == "${TOTAL}" ]] || DRIFT+="${file} says ${num}; "
done < <(cd "${REPO}" && grep -roE "[0-9]+ (self-tests|checks|headless checks)" \
         README.md CONTRIBUTING.md docs/index.html skills/ 2>/dev/null)
check "every documented check count is ${TOTAL}" "" "${DRIFT}"

# --------------------------------------------------------------------------- #
printf '\n%d passed, %d failed, %d skipped\n' "${PASS}" "${FAIL}" "${SKIP}"
[[ "${FAIL}" -eq 0 ]]
