CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/coding-policy

General-purpose coding policy for Baruch's AI agents

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Medium

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

test_check_git_sync.shhooks/tests/

#!/usr/bin/env bash
# Outcome-based tests for check-git-sync.sh.
#
# The hook shells out to real git, so tests build real local repos (a bare
# "origin" plus working clones) and drive git offline — no network, fully
# deterministic. The injected clock (SYNC_NOW) drives the throttle assertions.
#
# Each scenario builds its OWN bare origin + seed (mk_origin) so scenarios share
# no mutable state and run in any order (rules/testing-standards.md Independence).
# The harness drops `set -e` to aggregate results, so every fixture-setup command
# is checked explicitly and aborts with a fatal diagnostic on failure
# (rules/error-handling.md aggregate-reporting carve-out).
#
# Covers:
#   1. Behind        -> fast-forwards the default branch (on it or off it); a
#                       Herdr worker only reports; a refused fast-forward reports.
#   2. Up to date    -> emits a marker "in sync" status, exit 0.
#   3. Throttle      -> with a fixed injected clock: a call inside the window
#                       skips the fetch and reports "not verified" (never a
#                       definitive "in sync"/"behind" against the stale ref); a
#                       call past the window fetches and fires "behind".
#   4. Not a repo    -> silent no-op, exit 0.
#   5. No origin     -> silent no-op, exit 0.
#   6. Fetch failure -> marker "not verified" (never a false "in sync" against a
#                       stale ref), exit 0 (offline/broken remote tolerated).
#   7. Bad clock     -> silent no-op, exit 0 (never aborts SessionStart).
#   8. Diverged      -> marker notice names divergence and recommends rebase, not
#                       a fast-forward (local both ahead and behind origin).
#  10. Worker fetch -> a Herdr worker session leaves refs/remotes/origin/*
#                       unchanged while origin has moved; the foreman fetches.
#  11. Role unknown -> HERDR_ENV set and the role probes fail (git shim): no
#                       fetch (refs unchanged) and a sync-not-verified notice.
#  12. Empty HERDR_ENV -> set-but-empty still marks Herdr: a linked worktree
#                       does not fetch.
#  13. Portable worktree -> SESSION_START_MODE=portable in a linked worktree
#                       (env stripped by tessl): no fetch, sync not verified.
#   9. Future stamp  -> a schema_version > 1 record is not throttled on and is
#                       preserved (not downgraded to version 1).
#
# Run: bash hooks/tests/test_check_git_sync.sh
set -uo pipefail

die() { echo "fatal: $*" >&2; exit 2; }

# 0 when <a> and <b> name the same commit in <repo>, 1 when they differ. Called
# in the harness's own shell, never in $(...): a ref that does not resolve stops
# the harness, so an unresolved ref can never read as "different".
same_commit() { # <repo> <a> <b>
  local x y
  x="$(git -C "$1" rev-parse --verify --quiet "$2^{commit}")" || die "cannot resolve $2 in $1"
  y="$(git -C "$1" rev-parse --verify --quiet "$3^{commit}")" || die "cannot resolve $3 in $1"
  [[ "$x" == "$y" ]]
}

cleanup() { [[ -n "${TMP:-}" ]] && ! rm -rf "$TMP" && echo "warn: could not remove $TMP" >&2; return 0; }

g() { git "$@"; }

commit_push() { # <clone-dir> <message>
  local dir="$1" msg="$2"
  printf '%s\n' "$msg" >> "$dir/f"                || die "commit_push: write to $dir/f failed"
  g -C "$dir" add f                               || die "commit_push: git add failed in $dir"
  g -C "$dir" commit -q -m "$msg"                 || die "commit_push: git commit failed in $dir"
  g -C "$dir" push -q origin main                 || die "commit_push: git push from $dir failed"
}

mk_origin() { # <prefix>: sets globals BARE, SEED to a fresh, independent origin
  local prefix="$1"
  BARE="$TMP/${prefix}.git"; SEED="$TMP/${prefix}-seed"
  g init -q --bare -b main "$BARE"                || die "mk_origin: git init --bare failed for $BARE"
  g clone -q "$BARE" "$SEED" 2>/dev/null          || die "mk_origin: git clone failed for $SEED"
  g -C "$SEED" symbolic-ref HEAD refs/heads/main  || die "mk_origin: git symbolic-ref failed in $SEED"
  commit_push "$SEED" "c1"
}

clone_from() { # <bare> <dest>: a working clone, checked explicitly
  g clone -q "$1" "$2"                            || die "clone_from: git clone $1 -> $2 failed"
}

# run <repo-dir> <state-dir> [extra env...] -> OUT, RC
run() {
  local repo="$1" state="$2"; shift 2
  # Scrub the ambient HERDR_ENV so a suite run from a Herdr pane still sees
  # the non-Herdr cases as non-Herdr; a case opts in by passing it in "$@".
  OUT="$(cd "$repo" && env -u HERDR_ENV SYNC_STATE_DIR="$state" "$@" bash "$SCRIPT" </dev/null 2>/dev/null)"
  RC=$?
}

pass() { PASS=$((PASS+1)); }
fail() { FAIL=$((FAIL+1)); echo "  ✗ FAIL: $1" >&2; }

main() {
  SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/check-git-sync.sh"
  [[ -f "$SCRIPT" && -r "$SCRIPT" ]] || die "hook not found/readable at $SCRIPT"
  command -v jq  >/dev/null 2>&1 || die "jq required for these tests"
  command -v git >/dev/null 2>&1 || die "git required for these tests"

  TMP="$(mktemp -d -t git-sync-test.XXXXXX)" || die "mktemp failed"
  trap cleanup EXIT

  # Isolate git from the operator's global/system config so identity and defaults
  # are deterministic across machines. This isolation is load-bearing — an
  # unchecked mkdir failure would silently defeat it, so guard it explicitly.
  export HOME="$TMP/home"
  mkdir -p "$HOME" || die "could not create isolated HOME at $HOME"
  export GIT_CONFIG_NOSYSTEM=1
  export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t

  # An ambient HERDR_ENV, as when the suite runs from a Herdr pane: every
  # non-Herdr case now depends on run's scrub, and each Herdr case sets its own.
  export HERDR_ENV=1

  FAIL=0; PASS=0

  # 1. Behind, read first from a linked worktree with HERDR_ENV set -- a Herdr
  # worker. The shared checkout is the foreman's (skills/herdr-foreman/references/team-operation.md
  # Writers and Checkouts), so the hook reports the drift and moves nothing.
  mk_origin o1
  clone_from "$BARE" "$TMP/r1"
  commit_push "$SEED" "c2"
  # The foreman's earlier fetch: the worker reads drift from these refs as-is.
  git -C "$TMP/r1" fetch -q origin || die "r1 fetch failed"
  git -C "$TMP/r1" worktree add -q "$TMP/r1-wt" -b feat/worker >/dev/null 2>&1 \
    || die "r1 worktree add failed"
  run "$TMP/r1-wt" "$TMP/s1b" HERDR_ENV=1
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Herdr worker session") and test("1 behind") and test("Do not sync") and (test("fast-forward") | not)' >/dev/null 2>&1 \
    && ! same_commit "$TMP/r1" main origin/main; then
    pass; else fail "worker session: expected a no-sync notice and main unmoved, got RC=$RC OUT=$OUT"; fi

  # 1b. The same drift from the MAIN checkout (on main) is the foreman or a
  # standalone agent: the hook fast-forwards main and says so, without running
  # the repo's own post-merge hook.
  printf '#!/bin/sh\ntouch "%s"\n' "$TMP/post-merge-ran" > "$TMP/r1/.git/hooks/post-merge" \
    || die "could not write the post-merge hook"
  chmod +x "$TMP/r1/.git/hooks/post-merge" || die "could not make the post-merge hook executable"
  run "$TMP/r1" "$TMP/s1c" HERDR_ENV=1
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("fast-forwarded local `main` by 1")' >/dev/null 2>&1 \
    && same_commit "$TMP/r1" main origin/main \
    && [[ ! -e "$TMP/post-merge-ran" ]]; then
    pass; else fail "behind on main: expected a fast-forward with no repo hook run, got RC=$RC OUT=$OUT"; fi

  # 1e. Behind under tessl (session-start portable mode): report, never move.
  mk_origin o1e
  clone_from "$BARE" "$TMP/r1e"
  commit_push "$SEED" "c2"
  run "$TMP/r1e" "$TMP/s1f" SESSION_START_MODE=portable
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("not fast-forwarded here")' >/dev/null 2>&1 \
    && ! same_commit "$TMP/r1e" main origin/main; then
    pass; else fail "portable: expected a report and main unmoved, got RC=$RC OUT=$OUT"; fi

  # 1f. The fetch never runs repo code: a reference-transaction hook, which a
  #     ref-updating fetch triggers, leaves no marker.
  mk_origin o1f
  clone_from "$BARE" "$TMP/r1f"
  commit_push "$SEED" "c2"
  printf '#!/bin/sh\ntouch "%s"\n' "$TMP/sync-ref-hook-ran" > "$TMP/r1f/.git/hooks/reference-transaction" \
    || die "could not write the reference-transaction hook"
  chmod +x "$TMP/r1f/.git/hooks/reference-transaction" || die "could not make the hook executable"
  run "$TMP/r1f" "$TMP/s1g"
  if [[ $RC -eq 0 && ! -e "$TMP/sync-ref-hook-ran" ]]; then
    pass; else fail "repo hooks: the reference-transaction hook ran during the sync fetch or fast-forward (OUT=$OUT)"; fi

  # 1c. Behind while a feature branch is checked out: main moves without a
  # checkout, and the feature branch is untouched.
  mk_origin o1c
  clone_from "$BARE" "$TMP/r1c"
  git -C "$TMP/r1c" checkout -q -b feat/x || die "r1c checkout failed"
  commit_push "$SEED" "c2"
  run "$TMP/r1c" "$TMP/s1d"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1 \
    && same_commit "$TMP/r1c" main origin/main \
    && [[ "$(git -C "$TMP/r1c" symbolic-ref --short HEAD)" == "feat/x" ]]; then
    pass; else fail "behind off main: expected main fast-forwarded in place, got RC=$RC OUT=$OUT"; fi

  # 1d. Behind on main with a local edit the incoming commit would overwrite:
  # git refuses, the hook reports it, and the edit survives.
  mk_origin o1d
  clone_from "$BARE" "$TMP/r1d"
  printf 'local edit\n' >> "$TMP/r1d/f" || die "r1d edit failed"
  commit_push "$SEED" "c2"
  run "$TMP/r1d" "$TMP/s1e"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forward was refused")' >/dev/null 2>&1 \
    && grep -q "local edit" "$TMP/r1d/f" \
    && ! same_commit "$TMP/r1d" main origin/main; then
    pass; else fail "refused fast-forward: expected a report and the edit kept, got RC=$RC OUT=$OUT"; fi

  # 2. up to date -> marker "in sync" status.
  mk_origin o2
  clone_from "$BARE" "$TMP/r2"
  run "$TMP/r2" "$TMP/s2"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("in sync")' >/dev/null 2>&1; then
    pass; else fail "up-to-date: expected in-sync status, got RC=$RC OUT=$OUT"; fi

  # 2b. ahead only -> "ahead ... unpushed", never "in sync" (ahead>0, behind==0).
  #     Clone up to date, add a local commit without pushing; origin unchanged.
  mk_origin o2b
  clone_from "$BARE" "$TMP/r2b"
  g -C "$TMP/r2b" commit -q --allow-empty -m "local ahead"  || die "git commit in r2b failed"
  run "$TMP/r2b" "$TMP/s2b"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("ahead") and (test("in sync") | not)' >/dev/null 2>&1; then
    pass; else fail "ahead-only: expected \"ahead\" (not \"in sync\"), got RC=$RC OUT=$OUT"; fi

  # 3. throttle. Clone up to date. First call fetches, stamps, and reports a
  #    definitive "in sync". Move origin ahead WITHOUT the hook fetching: a call
  #    +60s is throttled, so it does not fetch and reports "not verified" (never
  #    a definitive "in sync"/"behind" against a stale ref). A call past the 1h
  #    window fetches the new commit and fires "behind".
  mk_origin o3
  clone_from "$BARE" "$TMP/r3"
  run "$TMP/r3" "$TMP/s3" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2000000               # fetch, stamp, up to date -> in sync
  # This establishes the throttle stamp the next two assertions depend on, so a
  # failure here must abort, not merely tally (aggregate-reporting carve-out:
  # later checks may not depend on an earlier one merely having incremented FAIL).
  { [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("in sync")' >/dev/null 2>&1; } \
    || die "throttle setup: first call should report in sync, got RC=$RC OUT=$OUT"
  commit_push "$SEED" "c3"                                # origin moves; r3's tracking ref still old
  run "$TMP/r3" "$TMP/s3" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2000060               # +60s: throttled -> no fetch -> not verified
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("not verified") and (test("in sync") | not) and (test("behind") | not)' >/dev/null 2>&1; then
    pass; else fail "throttle active: inside window should report 'not verified' (not 'in sync'/'behind'), got OUT=$OUT"; fi
  run "$TMP/r3" "$TMP/s3" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2003601               # +>1h: fetch -> behind -> fast-forwards
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1; then
    pass; else fail "throttle expired: past window should fetch and fire, got OUT=$OUT"; fi

  # 3b. By default the hook fetches every session: a recent stamp does not stop
  #     it, so origin moving is seen and fast-forwarded, never "not verified".
  mk_origin o3b
  clone_from "$BARE" "$TMP/r3b"
  run "$TMP/r3b" "$TMP/s3b" SYNC_NOW=2000000
  commit_push "$SEED" "c2"
  run "$TMP/r3b" "$TMP/s3b" SYNC_NOW=2000060
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1; then
    pass; else fail "default: expected a fetch every session, got OUT=$OUT"; fi

  # 4. not a repo -> silent no-op.
  mkdir -p "$TMP/notrepo" || die "could not create $TMP/notrepo"
  run "$TMP/notrepo" "$TMP/s4"
  if [[ $RC -eq 0 && -z "$OUT" ]]; then pass; else fail "not a repo: expected silent exit 0, got RC=$RC OUT=$OUT"; fi

  # 5. no origin remote -> silent no-op.
  g init -q -b main "$TMP/noorigin" || die "git init noorigin failed"
  printf 'x\n' > "$TMP/noorigin/f"  || die "write noorigin/f failed"
  g -C "$TMP/noorigin" add f        || die "git add in noorigin failed"
  g -C "$TMP/noorigin" commit -q -m x || die "git commit in noorigin failed"
  run "$TMP/noorigin" "$TMP/s5"
  if [[ $RC -eq 0 && -z "$OUT" ]]; then pass; else fail "no origin: expected silent exit 0, got RC=$RC OUT=$OUT"; fi

  # 6. fetch failure -> "not verified" (broken remote tolerated, no crash, and
  #    never a false "in sync" against a stale ref — sync-before-work). Clone,
  #    then delete the bare origin so the fetch fails; the hook reports
  #    not-verified and exits 0 without crashing.
  mk_origin o6
  clone_from "$BARE" "$TMP/r6"
  rm -rf "$BARE" || die "could not remove $BARE"
  run "$TMP/r6" "$TMP/s6"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("not verified") and (test("in sync") | not)' >/dev/null 2>&1; then
    pass; else fail "fetch failure: expected 'not verified' (not 'in sync') exit 0, got RC=$RC OUT=$OUT"; fi

  # 7. malformed clock -> no-op, exit 0.
  mk_origin o7
  clone_from "$BARE" "$TMP/r7"
  run "$TMP/r7" "$TMP/s7" SYNC_NOW="not-a-number"
  if [[ $RC -eq 0 && -z "$OUT" ]]; then pass; else fail "bad clock: expected silent exit 0, got RC=$RC OUT=$OUT"; fi

  # 8. diverged -> divergence notice (rebase, not fast-forward). Clone, add a
  #    local commit (ahead by 1), and push a different commit to origin (behind
  #    by 1); the hook fetches and reports the diverged state.
  mk_origin o8
  clone_from "$BARE" "$TMP/r8"
  printf 'local\n' >> "$TMP/r8/f"        || die "write r8/f failed"
  g -C "$TMP/r8" add f                    || die "git add in r8 failed"
  g -C "$TMP/r8" commit -q -m local       || die "git commit in r8 failed"   # ahead by 1
  commit_push "$SEED" "c2"                                                    # origin moves -> behind by 1
  run "$TMP/r8" "$TMP/s8"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("diverged") and test("rebase")' >/dev/null 2>&1; then
    pass; else fail "diverged: expected marker divergence notice, got RC=$RC OUT=$OUT"; fi

  # 9. future-version throttle stamp -> not throttled on, and preserved (never
  #    downgraded). Pre-seed a "2 <recent>" record at the stamp path the hook
  #    derives (cksum of the repo toplevel), move origin ahead, and run inside
  #    the window: a v1 stamp would throttle to silence, but the future record
  #    must be ignored (the hook fires) and left untouched.
  mk_origin o9
  clone_from "$BARE" "$TMP/r9"
  commit_push "$SEED" "c2"                                # origin ahead -> a non-throttled run fires
  local top9 key9 stampdir9 sv9
  top9="$(cd "$TMP/r9" && git rev-parse --show-toplevel)" || die "r9 toplevel failed"
  key9="$(printf '%s' "$top9" | cksum | cut -d' ' -f1)"   || die "r9 key derivation failed"
  stampdir9="$TMP/s9"
  mkdir -p "$stampdir9" || die "could not create $stampdir9"
  printf '2 %s\n' 2000000 > "$stampdir9/sync-$key9" || die "could not seed future stamp"
  run "$TMP/r9" "$stampdir9" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2000060            # within window, but future schema
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1; then
    pass; else fail "future stamp: expected fire (not throttled), got RC=$RC OUT=$OUT"; fi
  sv9=""; read -r sv9 _ < "$stampdir9/sync-$key9" || sv9=""
  if [[ "$sv9" == "2" ]]; then pass; else fail "future stamp: expected preserved version 2, got '$sv9'"; fi

  # 10. A Herdr worker never fetches: the remote-tracking refs are shared with
  #     the foreman's checkout. Origin moves, the worker session runs, and
  #     refs/remotes/origin/* is byte-for-byte what it was; the notice still
  #     tells the worker not to sync. The foreman in the main checkout then
  #     fetches and sees the new tip.
  mk_origin o10
  clone_from "$BARE" "$TMP/r10"
  git -C "$TMP/r10" worktree add -q "$TMP/r10-wt" -b feat/worker >/dev/null 2>&1 \
    || die "r10 worktree add failed"
  commit_push "$SEED" "c2"
  local refs10_before refs10_after refs10_foreman seed10 tip10
  refs10_before="$(git -C "$TMP/r10" for-each-ref refs/remotes/origin)" || die "r10 for-each-ref (before) failed"
  [[ -n "$refs10_before" ]] || die "r10 has no refs/remotes/origin refs to compare"
  run "$TMP/r10-wt" "$TMP/s10" HERDR_ENV=1
  refs10_after="$(git -C "$TMP/r10" for-each-ref refs/remotes/origin)" || die "r10 for-each-ref (after worker) failed"
  if [[ "$refs10_before" == "$refs10_after" ]]; then
    pass; else fail "worker fetch: refs/remotes/origin changed in a worker session (before=$refs10_before after=$refs10_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Herdr worker session") and test("does not fetch") and test("Do not sync")' >/dev/null 2>&1; then
    pass; else fail "worker fetch: expected the no-fetch worker notice, got RC=$RC OUT=$OUT"; fi
  run "$TMP/r10" "$TMP/s10f" HERDR_ENV=1
  refs10_foreman="$(git -C "$TMP/r10" for-each-ref refs/remotes/origin)" || die "r10 for-each-ref (after foreman) failed"
  seed10="$(git -C "$SEED" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in $SEED"
  tip10="$(git -C "$TMP/r10" rev-parse --verify --quiet 'origin/main^{commit}')" || die "cannot resolve origin/main in r10"
  if [[ $RC -eq 0 && "$refs10_foreman" != "$refs10_before" && "$tip10" == "$seed10" ]]; then
    pass; else fail "foreman fetch: expected the main checkout to fetch the moved origin, got RC=$RC OUT=$OUT"; fi

  # 11. HERDR_ENV set but the role probes fail: the session may be a worker,
  #     so the hook neither fetches nor fast-forwards, and says how to
  #     diagnose. A git shim on PATH fails only the two role probes.
  mk_origin o11
  clone_from "$BARE" "$TMP/r11"
  commit_push "$SEED" "c2"
  local realgit refs11_before refs11_after main11_before main11_after
  realgit="$(command -v git)" || die "cannot locate git"
  mkdir -p "$TMP/shim11" || die "could not create $TMP/shim11"
  # shellcheck disable=SC2016  # the shim's own "$@"/"$a" must stay literal in its source
  printf '#!/usr/bin/env bash\nfor a in "$@"; do case "$a" in --absolute-git-dir|--git-common-dir) echo "shim: role probe refused" >&2; exit 1 ;; esac; done\nexec %q "$@"\n' "$realgit" > "$TMP/shim11/git" \
    || die "could not write the git shim"
  chmod +x "$TMP/shim11/git" || die "could not make the git shim executable"
  refs11_before="$(git -C "$TMP/r11" for-each-ref refs/remotes/origin)" || die "r11 for-each-ref (before) failed"
  [[ -n "$refs11_before" ]] || die "r11 has no refs/remotes/origin refs to compare"
  main11_before="$(git -C "$TMP/r11" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r11"
  run "$TMP/r11" "$TMP/s11" HERDR_ENV=1 PATH="$TMP/shim11:$PATH"
  refs11_after="$(git -C "$TMP/r11" for-each-ref refs/remotes/origin)" || die "r11 for-each-ref (after) failed"
  main11_after="$(git -C "$TMP/r11" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r11 after the run"
  if [[ "$refs11_before" == "$refs11_after" ]]; then
    pass; else fail "role unknown: refs/remotes/origin changed (before=$refs11_before after=$refs11_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("sync not verified") and test("could not tell a Herdr worker") and test("git rev-parse --absolute-git-dir") and (test("fast-forwarded local") | not)' >/dev/null 2>&1 \
    && [[ "$main11_before" == "$main11_after" ]]; then
    pass; else fail "role unknown: expected a no-fetch sync-not-verified notice, got RC=$RC OUT=$OUT"; fi

  # 12. HERDR_ENV set but empty is still a Herdr session (rules/agent-team-operation.md
  #     Two Modes): a linked worktree is a worker and never fetches.
  mk_origin o12
  clone_from "$BARE" "$TMP/r12"
  git -C "$TMP/r12" worktree add -q "$TMP/r12-wt" -b feat/worker >/dev/null 2>&1 \
    || die "r12 worktree add failed"
  commit_push "$SEED" "c2"
  local refs12_before refs12_after
  refs12_before="$(git -C "$TMP/r12" for-each-ref refs/remotes/origin)" || die "r12 for-each-ref (before) failed"
  [[ -n "$refs12_before" ]] || die "r12 has no refs/remotes/origin refs to compare"
  run "$TMP/r12-wt" "$TMP/s12" HERDR_ENV=
  refs12_after="$(git -C "$TMP/r12" for-each-ref refs/remotes/origin)" || die "r12 for-each-ref (after) failed"
  if [[ "$refs12_before" == "$refs12_after" ]]; then
    pass; else fail "empty HERDR_ENV: refs/remotes/origin changed (before=$refs12_before after=$refs12_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Herdr worker session") and test("Do not sync")' >/dev/null 2>&1; then
    pass; else fail "empty HERDR_ENV: expected the worker notice, got RC=$RC OUT=$OUT"; fi

  # 13. Under tessl the environment is stripped, so a linked worktree may be a
  #     worker's: no fetch, no fast-forward, and a sync-not-verified notice.
  mk_origin o13
  clone_from "$BARE" "$TMP/r13"
  git -C "$TMP/r13" worktree add -q "$TMP/r13-wt" -b feat/maybe-worker >/dev/null 2>&1 \
    || die "r13 worktree add failed"
  commit_push "$SEED" "c2"
  local refs13_before refs13_after main13_before main13_after
  refs13_before="$(git -C "$TMP/r13" for-each-ref refs/remotes/origin)" || die "r13 for-each-ref (before) failed"
  [[ -n "$refs13_before" ]] || die "r13 has no refs/remotes/origin refs to compare"
  main13_before="$(git -C "$TMP/r13" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r13"
  run "$TMP/r13-wt" "$TMP/s13" SESSION_START_MODE=portable
  refs13_after="$(git -C "$TMP/r13" for-each-ref refs/remotes/origin)" || die "r13 for-each-ref (after) failed"
  main13_after="$(git -C "$TMP/r13" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r13 after the run"
  if [[ "$refs13_before" == "$refs13_after" && "$main13_before" == "$main13_after" ]]; then
    pass; else fail "portable worktree: refs or main changed (refs before=$refs13_before after=$refs13_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("sync not verified") and test("tessl") and test("Herdr worker")' >/dev/null 2>&1; then
    pass; else fail "portable worktree: expected a sync-not-verified notice, got RC=$RC OUT=$OUT"; fi

  echo "─────────────────────────────────────────────" >&2
  if [[ $FAIL -gt 0 ]]; then echo "FAILED: ${FAIL} failed, ${PASS} passed" >&2; exit 1; fi
  echo "PASSED: all ${PASS} checks" >&2
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi

README.md

tile.json