diff --git a/.agents/skills/running-tests/SKILL.md b/.agents/skills/running-tests/SKILL.md index a944f70c..b01f20b7 100644 --- a/.agents/skills/running-tests/SKILL.md +++ b/.agents/skills/running-tests/SKILL.md @@ -62,6 +62,12 @@ because they run serially on separate runners; `--snapshot-shard 1/2` and `2/2` exist for that topology, not same-machine parallelism. See [`Shared/SnapshotKitTesting/AGENTS.md`](../../../Shared/SnapshotKitTesting/AGENTS.md). +Every snapshot scope preflights the tracked references before generating or +building. If any are still Git LFS pointer text, `./test` fails with +`git lfs pull` rather than producing misleading "incomparable" results. +Codex-managed macOS setup hydrates them through `./ide --bootstrap --no-open`; +run `git lfs pull` explicitly for a manually-created or interrupted checkout. + ## Iterate faster After a green build: diff --git a/AGENTS.md b/AGENTS.md index f2cf0499..c0aba1f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -557,8 +557,9 @@ idempotent and regenerate it through the ChatGPT desktop app's local environment editor when changing its schema. - macOS setup runs `./ide --bootstrap --no-open`; bootstrap trusts the new - checkout's `.mise.toml`, installs pinned tools, syncs agent files, and - generates without opening Xcode. + checkout's `.mise.toml`, installs pinned tools, hydrates every Git LFS object + referenced by the checkout, syncs agent files, and generates without opening + Xcode. - Linux setup delegates to [`.cursor/install.sh`](.cursor/install.sh), with the same platform limits documented below. - Setup first runs `./worktree --check-main`, which refreshes `origin/main` and diff --git a/README.md b/README.md index 1f9a8f8b..b646f7ce 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,9 @@ Codex-managed worktrees use the checked-in local environment at without changing the checkout when its `HEAD` does not contain the latest main. The **Update to latest main** toolbar action safely fast-forwards a checkout directly behind main and refuses divergent feature history. On macOS the -environment also runs `./ide --bootstrap --no-open`, offers project generation, -affected tests, and format lint actions, and removes only that checkout's +environment also runs `./ide --bootstrap --no-open`, which hydrates the +checkout's Git LFS snapshot references before generating the project; it offers +affected tests and format lint actions, and removes only that checkout's simulator on cleanup. `.worktreeinclude` copies the gitignored `.mise.local.toml` signing override from the source checkout into each new managed worktree. diff --git a/ide b/ide index ad40eb26..74cdb9d4 100755 --- a/ide +++ b/ide @@ -150,6 +150,16 @@ EOF # Install the pinned tools (Tuist, SwiftFormat, Ruby) from .mise.toml. echo "==> mise install" "$MISE" install + + # Codex may create a managed worktree with LFS smudging disabled, leaving + # snapshot references as pointer text even though git-lfs is installed. + # Bootstrap is already the networked, one-shot setup boundary, and all + # worktrees share the repository's LFS object cache, so hydrate the current + # checkout here deterministically. A failed download must fail setup: an + # apparently ready checkout with pointer-backed references cannot run or + # re-record snapshots honestly. + echo "==> git lfs pull" + git lfs pull fi MISE="$(find_mise)" diff --git a/test b/test index b7881fe4..87fb1b8a 100755 --- a/test +++ b/test @@ -580,6 +580,52 @@ add_scheme() { SCHEMES+=("$candidate") } +require_hydrated_snapshot_references() { + local reference lfs_files_json + local pointers=() + + if ! command -v git-lfs >/dev/null 2>&1; then + echo "error: snapshot tests require git-lfs; run ./ide --bootstrap." >&2 + exit 1 + fi + + # One stable JSON inventory avoids starting `git-lfs` once per reference — + # hundreds of subprocesses and a multi-second tax on every snapshot run. + # `checkout` distinguishes hydrated full objects from pointer text. + lfs_files_json="$WORKDIR/lfs-files.json" + if ! git lfs ls-files --json >"$lfs_files_json"; then + echo "error: could not inspect Git LFS snapshot references." >&2 + exit 1 + fi + while IFS= read -r reference; do + [ -n "$reference" ] && pointers+=("$reference") + done < <(LFS_FILES_JSON="$lfs_files_json" python3 - <<'PY' +import json, os, pathlib + +inventory = json.loads(pathlib.Path(os.environ['LFS_FILES_JSON']).read_text()) +for item in inventory['files']: + name = item['name'] + if '/__Snapshots__/' in name and name.endswith('.png') and not item['checkout']: + print(name) +PY +) + + if [ "${#pointers[@]}" -eq 0 ]; then + return + fi + + echo "error: ${#pointers[@]} snapshot reference(s) are Git LFS pointer files, not PNGs." >&2 + echo " Snapshot comparisons would be incomplete or incomparable." >&2 + echo " Hydrate this checkout, then retry:" >&2 + echo " git lfs pull" >&2 + echo " First unresolved references:" >&2 + printf ' %s\n' "${pointers[@]:0:5}" >&2 + if [ "${#pointers[@]}" -gt 5 ]; then + echo " … and $((${#pointers[@]} - 5)) more" >&2 + fi + exit 1 +} + case "$SCOPE" in all) add_scheme "$UNIT_SCHEME" ;; snapshots) add_scheme "$SNAPSHOT_SCHEME" ;; @@ -666,11 +712,16 @@ if [ "${#SCHEMES[@]}" -eq 0 ]; then exit 1 fi +has_snapshot_scheme=false +for scheme in "${SCHEMES[@]}"; do + [ "$scheme" = "$SNAPSHOT_SCHEME" ] && has_snapshot_scheme=true +done + +if [ "$has_snapshot_scheme" = true ]; then + require_hydrated_snapshot_references +fi + if [ -n "$TIMING_REPORT" ]; then - has_snapshot_scheme=false - for scheme in "${SCHEMES[@]}"; do - [ "$scheme" = "$SNAPSHOT_SCHEME" ] && has_snapshot_scheme=true - done if [ "$has_snapshot_scheme" != true ]; then echo "error: --timing-report requires a snapshot test scope" >&2 exit 1