#!/usr/bin/env bash
# Regression tests for compact-context-restore.mjs — SessionStart hook (compact/startup/resume).
# Covers journal recovery, compact-proof structured Task* injection from disk, and fail-open edges.
# Run: bash test-compact-context-restore.sh (exit 0 = all pass).
set -uo pipefail
HOOK="$(cd "$(dirname "$0")" && pwd)/compact-context-restore.mjs"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf '  ok   %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf '  FAIL %s\n     %s\n' "$1" "$2"; }

SB=$(mktemp -d "${TMPDIR:-/tmp}/ccr-test-XXXX")
trap 'rm -rf "$SB"' EXIT
T="$SB/transcript.jsonl"
TASKS="$SB/tasks"
mkdir -p "$TASKS"
export CCR_TASKS_ROOT="$TASKS"
SID="sess-current"

run() { # run <source> [session_id] [transcript_path]
  local src="$1" sid="${2:-$SID}" tp="${3:-$T}"
  if [ -n "$tp" ]; then
    printf '{"session_id":"%s","transcript_path":"%s","hook_event_name":"SessionStart","source":"%s"}' "$sid" "$tp" "$src"
  else
    printf '{"session_id":"%s","hook_event_name":"SessionStart","source":"%s"}' "$sid" "$src"
  fi | node "$HOOK" \
    | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).hookSpecificOutput.additionalContext)}catch{}})'
}

write_task() { # write_task <session_id> <task_id> <json>
  local dir="$TASKS/$1"
  mkdir -p "$dir"
  printf '%s' "$3" > "$dir/$2.json"
}

reset_tasks() {
  rm -rf "$TASKS"
  mkdir -p "$TASKS"
}

NATIVE='{"type":"user","promptSource":"typed","timestamp":"2026-07-28T10:00:00Z","message":{"role":"user","content":"refactor the payment webhook handler to validate the signature before parsing the body, alpha marker"}}'
JOURNALED='{"type":"user","version":"live-transcript-journal/1","promptId":"p1","timestamp":"2026-07-28T10:00:30Z","message":{"role":"user","content":"add a regression test that the refund route rejects a replayed event id, bravo marker"}}'
TOOLRESULT='{"type":"user","version":"live-transcript-journal/1","timestamp":"2026-07-28T10:00:40Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t","content":"charlie marker tool output that is not a user prompt"}]}}'
INJECTED='{"type":"user","timestamp":"2026-07-28T10:00:50Z","message":{"role":"user","content":"delta marker harness injection carrying no human provenance stamp at all"}}'

# --- hook-owned mode: journal records live in the transcript itself ---
printf '%s\n%s\n%s\n%s\n' "$NATIVE" "$JOURNALED" "$TOOLRESULT" "$INJECTED" > "$T"
OUT=$(run compact)
case "$OUT" in *alpha*) ok "hook-owned: native typed prompt recovered";;
  *) bad "hook-owned: native typed prompt recovered" "alpha missing";; esac
case "$OUT" in *bravo*) ok "hook-owned: journaled prompt recovered";;
  *) bad "hook-owned: journaled prompt recovered" "bravo missing";; esac
case "$OUT" in *charlie*) bad "tool_result excluded" "charlie leaked into prompts";;
  *) ok "tool_result excluded";; esac
case "$OUT" in *delta*) bad "unstamped injection excluded" "delta leaked into prompts";;
  *) ok "unstamped injection excluded";; esac

# --- the real hook-owned shape: one prompt recorded by BOTH the harness and the
# journal, the native copy wrapped in a system-reminder the journal copy lacks ---
DUP_NATIVE='{"type":"user","promptSource":"typed","timestamp":"2026-07-28T11:00:00Z","message":{"role":"user","content":"<system-reminder>ambient context</system-reminder>echo marker prompt recorded by both the harness and the journal"}}'
DUP_JOURNAL='{"type":"user","version":"live-transcript-journal/1","promptId":"p9","timestamp":"2026-07-28T11:00:00Z","message":{"role":"user","content":"echo marker prompt recorded by both the harness and the journal"}}'
printf '%s\n%s\n' "$DUP_NATIVE" "$DUP_JOURNAL" > "$T"
N=$(run compact | grep -c 'echo marker')
if [ "$N" -eq 1 ]; then ok "dual-provenance prompt recovered exactly once"
else bad "dual-provenance prompt recovered exactly once" "appeared $N times"; fi

# --- sidecar mode: journal lands in <transcript>.live.jsonl ---
printf '%s\n' "$NATIVE" > "$T"
printf '%s\n%s\n' "$JOURNALED" "$TOOLRESULT" > "$T.live.jsonl"
OUT=$(run compact)
case "$OUT" in *bravo*) ok "sidecar: journaled prompt recovered";;
  *) bad "sidecar: journaled prompt recovered" "bravo missing";; esac
A=${OUT%%alpha*}; B=${OUT%%bravo*}
if [ ${#A} -lt ${#B} ]; then ok "sidecar: merged chronologically by timestamp"
else bad "sidecar: merged chronologically by timestamp" "bravo ordered before alpha"; fi

# --- window: opening turns survive a session long enough to overflow the tail ---
: > "$T"; rm -f "$T.live.jsonl"
for i in $(seq 1 60); do
  printf '{"type":"user","promptSource":"typed","timestamp":"2026-07-28T12:%02d:00Z","message":{"role":"user","content":"turn %s of the long session, a distinct sentence so the noise filter keeps it"}}\n' \
    $((i % 60)) "$i" >> "$T"
done
OUT=$(run compact)
for i in 1 2 3; do
  case "$OUT" in *"turn $i of"*) ok "long session: opening turn $i retained";;
    *) bad "long session: opening turn $i retained" "turn $i missing";; esac
done
case "$OUT" in *"turn 60 of"*) ok "long session: newest turn retained";;
  *) bad "long session: newest turn retained" "turn 60 missing";; esac
case "$OUT" in *"turn 20 of"*) bad "long session: middle turn dropped" "turn 20 present";;
  *) ok "long session: middle turn dropped";; esac
case "$OUT" in *"32 middle turns omitted"*) ok "long session: gap marker counts omissions";;
  *) bad "long session: gap marker counts omissions" "marker absent/miscounted";; esac
case "$OUT" in *"4. turn 36 of"*) bad "numbering" "renumbered from 1 across the gap";;
  *) ok "numbering follows original turn positions";; esac

# --- window: no gap marker when everything fits ---
: > "$T"
for i in 1 2 3 4 5; do
  printf '{"type":"user","promptSource":"typed","timestamp":"2026-07-28T13:%02d:00Z","message":{"role":"user","content":"short session turn %s with enough words to clear the noise filter"}}\n' "$i" "$i" >> "$T"
done
OUT=$(run compact)
case "$OUT" in *omitted*) bad "short session: no gap marker" "marker present";;
  *) ok "short session: no gap marker";; esac
case "$OUT" in *"turn 3 with"*) ok "short session: all turns retained";;
  *) bad "short session: all turns retained" "turn 3 missing";; esac

# --- per-position clipping: opening turns get the wide cap, tail turns the narrow one ---
: > "$T"; rm -f "$T.live.jsonl"
LONG=$(node -e 'process.stdout.write("opening "+"w".repeat(2500)+" ENDHEAD")')
TAIL=$(node -e 'process.stdout.write("tailturn "+"z".repeat(2500)+" ENDTAIL")')
printf '{"type":"user","promptSource":"typed","timestamp":"2026-07-28T14:00:00Z","message":{"role":"user","content":"%s"}}\n' "$LONG" > "$T"
for i in $(seq 2 30); do
  printf '{"type":"user","promptSource":"typed","timestamp":"2026-07-28T14:%02d:00Z","message":{"role":"user","content":"filler turn %s with plenty of words to clear the noise filter"}}\n' "$i" "$i" >> "$T"
done
printf '{"type":"user","promptSource":"typed","timestamp":"2026-07-28T15:00:00Z","message":{"role":"user","content":"%s"}}\n' "$TAIL" >> "$T"
OUT=$(run compact)
case "$OUT" in *ENDHEAD*) ok "opening turn kept whole under the wide cap";;
  *) bad "opening turn kept whole under the wide cap" "2.5k opener was clipped";; esac
case "$OUT" in *ENDTAIL*) bad "tail turn clipped at the narrow cap" "2.5k tail turn kept whole";;
  *) ok "tail turn clipped at the narrow cap";; esac

# --- queued prompts: typed mid-turn, delivered as a threaded attachment ---
QUEUED='{"type":"attachment","uuid":"u1","parentUuid":"p0","timestamp":"2026-07-28T16:00:00Z","attachment":{"type":"queued_command","commandMode":"prompt","origin":{"kind":"human"},"prompt":"foxtrot marker queued while the turn was still running"}}'
QUEUED_SLASH='{"type":"attachment","uuid":"u2","parentUuid":"p0","timestamp":"2026-07-28T16:00:10Z","attachment":{"type":"queued_command","commandMode":"task-notification","origin":{"kind":"human"},"prompt":"golf marker queued task notification, not a typed prompt"}}'
QUEUED_NOHUMAN='{"type":"attachment","uuid":"u3","parentUuid":"p0","timestamp":"2026-07-28T16:00:20Z","attachment":{"type":"queued_command","commandMode":"prompt","prompt":"hotel marker queued command carrying no human origin stamp"}}'
QUEUED_OTHER='{"type":"attachment","uuid":"u4","parentUuid":"p0","timestamp":"2026-07-28T16:00:30Z","attachment":{"type":"edited_text_file","commandMode":"prompt","origin":{"kind":"human"},"prompt":"india marker file attachment that is not a queued prompt"}}'
printf '%s\n%s\n%s\n%s\n' "$QUEUED" "$QUEUED_SLASH" "$QUEUED_NOHUMAN" "$QUEUED_OTHER" > "$T"
OUT=$(run compact)
case "$OUT" in *foxtrot*) ok "queued mid-turn prompt recovered";;
  *) bad "queued mid-turn prompt recovered" "foxtrot missing";; esac
case "$OUT" in *golf*) bad "queued task-notification excluded" "golf leaked into prompts";;
  *) ok "queued task-notification excluded";; esac
case "$OUT" in *hotel*) bad "queued non-human origin excluded" "hotel leaked into prompts";;
  *) ok "queued non-human origin excluded";; esac
case "$OUT" in *india*) bad "non-queued attachment excluded" "india leaked into prompts";;
  *) ok "non-queued attachment excluded";; esac

# --- a queued prompt the harness ALSO records as typed appears exactly once ---
DUP_Q='{"type":"attachment","uuid":"u5","parentUuid":"p0","timestamp":"2026-07-28T16:01:00Z","attachment":{"type":"queued_command","commandMode":"prompt","origin":{"kind":"human"},"prompt":"juliet marker queued prompt also recorded as typed"}}'
DUP_T='{"type":"user","promptSource":"typed","timestamp":"2026-07-28T16:01:05Z","message":{"role":"user","content":"juliet marker queued prompt also recorded as typed"}}'
printf '%s\n%s\n' "$DUP_Q" "$DUP_T" > "$T"
N=$(run compact | grep -c 'juliet marker')
if [ "$N" -eq 1 ]; then ok "queued+typed duplicate recovered exactly once"
else bad "queued+typed duplicate recovered exactly once" "appeared $N times"; fi

# --- AskUserQuestion decisions come from the journal's answered copy only ---
DEC_J='{"type":"assistant","version":"live-transcript-journal/1","timestamp":"2026-07-28T17:00:00Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"AskUserQuestion","input":{"questions":[{"question":"kilo marker which license should we ship under?"}],"answers":{"kilo marker which license should we ship under?":"Apache-2.0 lima marker"}}}]}}'
DEC_HARNESS='{"type":"assistant","version":"2.1.220","timestamp":"2026-07-28T17:00:05Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"AskUserQuestion","input":{"questions":[{"question":"mike marker unanswered harness copy of the question"}]}}]}}'
DEC_EMPTY='{"type":"assistant","version":"live-transcript-journal/1","timestamp":"2026-07-28T17:00:10Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"t3","name":"AskUserQuestion","input":{"questions":[{"question":"november marker declined question"}],"answers":{}}}]}}'
printf '%s\n%s\n%s\n' "$DEC_J" "$DEC_HARNESS" "$DEC_EMPTY" > "$T"
OUT=$(run compact)
case "$OUT" in *"kilo marker"*) ok "decision question recovered";;
  *) bad "decision question recovered" "kilo missing";; esac
case "$OUT" in *"lima marker"*) ok "decision answer recovered";;
  *) bad "decision answer recovered" "lima missing";; esac
case "$OUT" in *"mike marker"*) bad "unanswered harness copy excluded" "mike leaked in";;
  *) ok "unanswered harness copy excluded";; esac
case "$OUT" in *"november marker"*) bad "empty answers excluded" "november leaked in";;
  *) ok "empty answers excluded";; esac
case "$OUT" in *"do NOT re-ask"*) ok "decisions section labelled as settled";;
  *) bad "decisions section labelled as settled" "label missing";; esac

# --- a decision journaled twice is listed once; the cap keeps the newest ---
printf '%s\n%s\n' "$DEC_J" "$DEC_J" > "$T"
N=$(run compact | grep -c 'lima marker')
if [ "$N" -eq 1 ]; then ok "duplicate decision listed once"
else bad "duplicate decision listed once" "appeared $N times"; fi
: > "$T"
for i in $(seq 1 14); do
  printf '{"type":"assistant","version":"live-transcript-journal/1","timestamp":"2026-07-28T18:%02d:00Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"d%s","name":"AskUserQuestion","input":{"answers":{"decision question number %s":"answer number %s"}}}]}}\n' "$i" "$i" "$i" "$i" >> "$T"
done
OUT=$(run compact)
case "$OUT" in *"most recent 10 of 14"*) ok "decision cap reports what it dropped";;
  *) bad "decision cap reports what it dropped" "count line absent";; esac
case "$OUT" in *"answer number 14"*) ok "decision cap keeps the newest";;
  *) bad "decision cap keeps the newest" "newest decision dropped";; esac
case "$OUT" in *"answer number 1\""*|*"answer number 1 "*) bad "decision cap drops the oldest" "oldest decision present";;
  *) ok "decision cap drops the oldest";; esac

# --- structured tasks: persisted JSON task files ---
reset_tasks
write_task "$SID" 7 '{"id":7,"subject":"persisted json task","status":"pending","blockedBy":[]}'
OUT=$(run startup)
case "$OUT" in
  *"#7 [pending] persisted json task"*) ok "tasks: persisted json file discovered";;
  *) bad "tasks: persisted json file discovered" "task missing";;
esac

# --- structured tasks: exact open tasks on compact/startup/resume (transcript present) ---
reset_tasks
write_task "$SID" 2 '{"id":2,"subject":"bravo pending task","status":"pending","blockedBy":[]}'
write_task "$SID" 1 '{"id":1,"subject":"alpha in progress","description":"work details","status":"in_progress","blockedBy":[9]}'
for SRC in compact startup resume; do
  OUT=$(run "$SRC")
  case "$OUT" in
    *"from disk"*) ok "tasks $SRC: exact-session header";;
    *) bad "tasks $SRC: exact-session header" "disk header missing";;
  esac
  case "$OUT" in
    *"#1 [in_progress] alpha in progress"*) ok "tasks $SRC: in_progress line";;
    *) bad "tasks $SRC: in_progress line" "missing in_progress";;
  esac
  case "$OUT" in
    *"#2 [pending] bravo pending task"*) ok "tasks $SRC: pending line";;
    *) bad "tasks $SRC: pending line" "missing pending";;
  esac
done

# --- ordering: in_progress before pending; numeric ID; blocked-by; description truncation ---
reset_tasks
write_task "$SID" 10 '{"id":10,"subject":"pending ten","status":"pending","blockedBy":[]}'
write_task "$SID" 3 '{"id":3,"subject":"progress three","description":"short","status":"in_progress","blockedBy":[1,2]}'
write_task "$SID" 5 '{"id":5,"subject":"progress five","status":"in_progress","blockedBy":[]}'
OUT=$(run startup)
P3=$(printf '%s' "$OUT" | grep -n '#3 \[in_progress\]' | cut -d: -f1)
P5=$(printf '%s' "$OUT" | grep -n '#5 \[in_progress\]' | cut -d: -f1)
P10=$(printf '%s' "$OUT" | grep -n '#10 \[pending\]' | cut -d: -f1)
if [ -n "$P3" ] && [ -n "$P5" ] && [ -n "$P10" ] && [ "$P3" -lt "$P5" ] && [ "$P5" -lt "$P10" ]; then
  ok "tasks: in_progress before pending; numeric ID order"
else
  bad "tasks: in_progress before pending; numeric ID order" "order wrong: p3=$P3 p5=$P5 p10=$P10"
fi
case "$OUT" in
  *"(blocked by #1,#2)"*) ok "tasks: blocked-by suffix";;
  *) bad "tasks: blocked-by suffix" "blocked-by missing";;
esac
LONG_DESC=$(node -e 'process.stdout.write("d".repeat(400))')
write_task "$SID" 7 '{"id":7,"subject":"desc truncate","description":"'"$LONG_DESC"'","status":"in_progress","blockedBy":[]}'
OUT=$(run startup)
DESC_LINE=$(printf '%s' "$OUT" | grep '#7 \[in_progress\]' -A1 | grep 'description:')
DESC_BYTES=$(printf '%s' "$DESC_LINE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const p=s.indexOf("description: ")+13;process.stdout.write(String(Buffer.byteLength(s.slice(p).trim(),"utf8")))})')
if [ "$DESC_BYTES" -le 300 ]; then ok "tasks: description truncated to 300 utf8 bytes"
else bad "tasks: description truncated to 300 utf8 bytes" "got $DESC_BYTES bytes"; fi

# --- exact open tasks without transcript ---
reset_tasks
write_task "$SID" 1 '{"id":1,"subject":"no transcript task","status":"pending","blockedBy":[]}'
OUT=$(run startup "$SID" "")
case "$OUT" in
  *"#1 [pending] no transcript task"*) ok "tasks: exact session without transcript";;
  *) bad "tasks: exact session without transcript" "task missing";;
esac

# --- completed/deleted excluded; valid empty exact store emits nothing ---
reset_tasks
write_task "$SID" 1 '{"id":1,"subject":"done task","status":"completed","blockedBy":[]}'
write_task "$SID" 2 '{"id":2,"subject":"deleted task","status":"deleted","blockedBy":[]}'
if [ -z "$(run startup)" ]; then ok "tasks: completed/deleted only emits nothing"
else bad "tasks: completed/deleted only emits nothing" "unexpected output"; fi
write_task "$SID" 3 '{"id":3,"subject":"open one","status":"pending","blockedBy":[]}'
OUT=$(run startup)
case "$OUT" in
  *"#3 [pending] open one"*) ok "tasks: open sibling among closed";;
  *) bad "tasks: open sibling among closed" "open task missing";;
esac
reset_tasks
write_task "$SID" 9 '{"id":9,"subject":"all done","status":"completed","blockedBy":[]}'
if [ -z "$(run startup)" ]; then ok "tasks: valid empty exact store silent"
else bad "tasks: valid empty exact store silent" "should not fall back"; fi

# --- fallback: one older sibling recovers and names source session ---
reset_tasks
OLD="sess-old"
write_task "$OLD" 4 '{"id":4,"subject":"recovered task","status":"in_progress","blockedBy":[]}'
printf '%s\n' "$NATIVE" > "$SB/$OLD.jsonl"
touch -d '2020-01-01' "$SB/$OLD.jsonl"
printf '%s\n' "$NATIVE" > "$T"
touch -d '2021-01-01' "$T"
OUT=$(run startup "$SID")
case "$OUT" in
  *"recovered from $OLD"*) ok "tasks: fallback names source session";;
  *) bad "tasks: fallback names source session" "header wrong: $OUT";;
esac
case "$OUT" in
  *"#4 [in_progress] recovered task"*) ok "tasks: fallback renders open tasks";;
  *) bad "tasks: fallback renders open tasks" "task missing";;
esac

# --- multiple eligible candidates fail closed; newer sibling ignored ---
reset_tasks
write_task "sess-a" 1 '{"id":1,"subject":"candidate a","status":"pending","blockedBy":[]}'
write_task "sess-b" 1 '{"id":1,"subject":"candidate b","status":"pending","blockedBy":[]}'
printf '{}' > "$SB/sess-a.jsonl"; touch -d '2020-01-01' "$SB/sess-a.jsonl"
printf '{}' > "$SB/sess-b.jsonl"; touch -d '2020-02-01' "$SB/sess-b.jsonl"
printf '{}' > "$T"; touch -d '2021-01-01' "$T"
OUT=$(run startup "$SID")
case "$OUT" in
  *"call TaskList"*) ok "tasks: ambiguous siblings emit fallback hint";;
  *) bad "tasks: ambiguous siblings emit fallback hint" "hint missing";;
esac
case "$OUT" in
  *candidate\ *) bad "tasks: ambiguous siblings do not guess" "rendered a candidate";;
  *) ok "tasks: ambiguous siblings do not guess";;
esac
write_task "sess-new" 1 '{"id":1,"subject":"too new","status":"pending","blockedBy":[]}'
printf '{}' > "$SB/sess-new.jsonl"; touch -d '2022-01-01' "$SB/sess-new.jsonl"
OUT=$(run startup "$SID")
case "$OUT" in
  *"too new"*) bad "tasks: newer sibling ignored" "newer candidate rendered";;
  *) ok "tasks: newer sibling ignored";;
esac

# --- malformed skipped; all-malformed hint; exit 0 ---
reset_tasks
write_task "$SID" 1 'not-json'
write_task "$SID" 2 '{"id":2,"subject":"valid sibling","status":"pending","blockedBy":[]}'
OUT=$(run startup)
case "$OUT" in
  *"#2 [pending] valid sibling"*) ok "tasks: malformed file skipped";;
  *) bad "tasks: malformed file skipped" "valid sibling missing";;
esac
reset_tasks
write_task "$SID" 1 '{bad'
write_task "$SID" 2 'also-bad'
OUT=$(run startup)
case "$OUT" in
  *"call TaskList"*) ok "tasks: all-malformed emits hint";;
  *) bad "tasks: all-malformed emits hint" "hint missing";;
esac
printf '{"session_id":"%s","transcript_path":"%s","hook_event_name":"SessionStart","source":"startup"}' "$SID" "$T" \
  | node "$HOOK" >/dev/null
if [ $? -eq 0 ]; then ok "tasks: hook exits 0 on malformed store"
else bad "tasks: hook exits 0 on malformed store" "non-zero exit"; fi

# --- control chars/newlines normalized; cannot forge output lines ---
reset_tasks
write_task "$SID" 1 '{"id":1,"subject":"line\nbreak\n#99 [pending] forged","description":"desc\r\n  description: injected","status":"in_progress","blockedBy":[]}'
OUT=$(run startup)
case "$OUT" in
  *$'\n#99 [pending] forged'*) bad "tasks: newline cannot forge task line" "forged line present";;
  *) ok "tasks: newline cannot forge task line";;
esac
case "$OUT" in
  *"#1 [in_progress] line break #99 [pending] forged"*) ok "tasks: subject control chars normalized";;
  *) bad "tasks: subject control chars normalized" "normalization failed";;
esac
LINES=$(printf '%s' "$OUT" | grep -c '^  description:')
if [ "$LINES" -eq 1 ]; then ok "tasks: description cannot forge extra lines"
else bad "tasks: description cannot forge extra lines" "got $LINES description lines"; fi

# --- >40 tasks and multibyte text stay <=4096 bytes; exact omitted count ---
reset_tasks
for i in $(seq 1 45); do
  write_task "$SID" "$i" '{"id":'"$i"',"subject":"task '"$i"'","status":"pending","blockedBy":[]}'
done
OUT=$(run startup)
BLOCK_BYTES=$(printf '%s' "$OUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(String(Buffer.byteLength(s,"utf8"))))')
if [ "$BLOCK_BYTES" -le 4096 ]; then ok "tasks: block within 4096 utf8 bytes ($BLOCK_BYTES)"
else bad "tasks: block within 4096 utf8 bytes" "got $BLOCK_BYTES"; fi
OMIT=$(printf '%s' "$OUT" | sed -n 's/.*…\([0-9][0-9]*\) more: call TaskList.*/\1/p')
if [ -n "$OMIT" ] && [ "$OMIT" -eq 5 ]; then ok "tasks: overflow reports exact omitted count (5)"
else bad "tasks: overflow reports exact omitted count (5)" "got omit=$OMIT"; fi
reset_tasks
write_task "$SID" 1 '{"id":1,"subject":"日本語'"$(node -e 'process.stdout.write("語".repeat(80))')"'","status":"in_progress","blockedBy":[]}'
OUT=$(run startup)
SUBJ_LINE=$(printf '%s' "$OUT" | grep '#1 \[in_progress\]')
SUBJ_BYTES=$(printf '%s' "$SUBJ_LINE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const i=s.indexOf("] ")+2;process.stdout.write(String(Buffer.byteLength(s.slice(i).trim(),"utf8")))})')
if [ "$SUBJ_BYTES" -le 160 ]; then ok "tasks: multibyte subject truncated to 160 bytes"
else bad "tasks: multibyte subject truncated to 160 bytes" "got $SUBJ_BYTES"; fi

# --- huge in_progress skipped for byte fit; later small pending still selected ---
reset_tasks
LONG_DESC=$(node -e 'process.stdout.write("d".repeat(400))')
LONG_SUBJ=$(node -e 'process.stdout.write("x".repeat(150))')
for i in $(seq 1 9); do
  write_task "$SID" "$i" '{"id":'"$i"',"subject":"'"$LONG_SUBJ"'","description":"'"$LONG_DESC"'","status":"in_progress","blockedBy":[]}'
  touch -d "2020-01-$(printf '%02d' "$i")" "$TASKS/$SID/$i.json"
done
for i in $(seq 10 17); do
  write_task "$SID" "$i" '{"id":'"$i"',"subject":"small task '"$i"'","status":"pending","blockedBy":[]}'
  touch -d "2020-02-01" "$TASKS/$SID/$i.json"
done
OUT=$(run startup)
case "$OUT" in
  *"#1 [in_progress]"*) bad "tasks: huge in_progress skipped for byte fit" "task 1 rendered";;
  *) ok "tasks: huge in_progress skipped for byte fit";;
esac
for i in 10 11 12; do
  case "$OUT" in
    *"#$i [pending] small task $i"*) ok "tasks: small pending $i selected after byte-fit skip";;
    *) bad "tasks: small pending $i selected after byte-fit skip" "task $i missing";;
  esac
done
OMIT=$(printf '%s' "$OUT" | sed -n 's/.*…\([0-9][0-9]*\) more: call TaskList.*/\1/p')
if [ -n "$OMIT" ] && [ "$OMIT" -eq 6 ]; then ok "tasks: byte-fit skip reports exact omitted count (6)"
else bad "tasks: byte-fit skip reports exact omitted count (6)" "got omit=$OMIT"; fi
BLOCK_BYTES=$(printf '%s' "$OUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(String(Buffer.byteLength(s,"utf8"))))')
if [ "$BLOCK_BYTES" -le 4096 ]; then ok "tasks: byte-fit skip block within 4096 utf8 bytes ($BLOCK_BYTES)"
else bad "tasks: byte-fit skip block within 4096 utf8 bytes" "got $BLOCK_BYTES"; fi

# --- invalid task/blockedBy IDs rejected ---
reset_tasks
write_task "$SID" 1 '{"id":-1,"subject":"negative id","status":"pending","blockedBy":[]}'
write_task "$SID" 2 '{"id":2,"subject":"valid sibling","status":"pending","blockedBy":[]}'
OUT=$(run startup)
case "$OUT" in
  *"#2 [pending] valid sibling"*) ok "tasks: negative task id rejected";;
  *) bad "tasks: negative task id rejected" "valid sibling missing";;
esac
case "$OUT" in
  *"negative id"*) bad "tasks: negative task id rejected" "negative id task rendered";;
  *) ok "tasks: negative task id not rendered";;
esac
write_task "$SID" 3 '{"id":3,"subject":"bad blockedBy","status":"pending","blockedBy":[-9,1.5,"x",2]}'
OUT=$(run startup)
case "$OUT" in
  *"(blocked by #2)"*) ok "tasks: invalid blockedBy ids filtered";;
  *) bad "tasks: invalid blockedBy ids filtered" "only valid blocker missing";;
esac
case "$OUT" in
  *"#-9"*|*"#1.5"*) bad "tasks: invalid blockedBy ids filtered" "invalid blocker rendered";;
  *) ok "tasks: invalid blockedBy ids not rendered";;
esac

# --- clear emits nothing ---
reset_tasks
write_task "$SID" 1 '{"id":1,"subject":"should not show","status":"pending","blockedBy":[]}'
if [ -z "$(run clear)" ]; then ok "source=clear injects nothing"
else bad "source=clear injects nothing" "hook emitted context"; fi

# --- compact keeps prompt/decision/TodoWrite recovery with rendered tasks ---
reset_tasks
write_task "$SID" 1 '{"id":1,"subject":"compact task","status":"pending","blockedBy":[]}'
printf '%s\n' "$NATIVE" "$DEC_J" > "$T"
TODO='{"type":"assistant","timestamp":"2026-07-28T19:00:00Z","message":{"role":"assistant","content":[{"type":"tool_use","name":"TodoWrite","input":{"todos":[{"content":"todo marker","status":"in_progress"}]}}]}}'
printf '%s\n' "$TODO" >> "$T"
OUT=$(run compact)
case "$OUT" in *alpha*) ok "compact+tasks: prompt recovery preserved";;
  *) bad "compact+tasks: prompt recovery preserved" "alpha missing";; esac
case "$OUT" in *"lima marker"*) ok "compact+tasks: decision recovery preserved";;
  *) bad "compact+tasks: decision recovery preserved" "lima missing";; esac
case "$OUT" in *"todo marker"*) ok "compact+tasks: TodoWrite recovery preserved";;
  *) bad "compact+tasks: TodoWrite recovery preserved" "todo missing";; esac
case "$OUT" in *"#1 [pending] compact task"*) ok "compact+tasks: rendered task block";;
  *) bad "compact+tasks: rendered task block" "task missing";; esac
case "$OUT" in *"from disk"*) ok "compact+tasks: disk header replaces old hint";;
  *) bad "compact+tasks: disk header replaces old hint" "header wrong";; esac

# --- missing transcript: compact still recovers tasks; no journal recovery ---
reset_tasks
write_task "$SID" 2 '{"id":2,"subject":"compact sans transcript","status":"pending","blockedBy":[]}'
T="$SB/absent.jsonl"
OUT=$(run compact "$SID" "$T")
case "$OUT" in
  *"#2 [pending] compact sans transcript"*) ok "compact: exact tasks without transcript";;
  *) bad "compact: exact tasks without transcript" "task missing";;
esac
case "$OUT" in
  *alpha*|*"VERBATIM JOURNAL"*) bad "compact: no journal recovery without transcript" "journal leaked";;
  *) ok "compact: no journal recovery without transcript";;
esac

# --- missing exact store with no safe fallback emits hint on startup ---
reset_tasks
OUT=$(run startup "$SID" "$SB/no-such-transcript.jsonl")
case "$OUT" in
  *"call TaskList"*) ok "tasks: missing store emits fallback hint";;
  *) bad "tasks: missing store emits fallback hint" "hint missing";;
esac

printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]
