Skip to content

Behavioral fixture suite + fix warm-reattach descendant walk#16

Closed
Christian-Katzmann wants to merge 1 commit into
mainfrom
add-fixture-suite
Closed

Behavioral fixture suite + fix warm-reattach descendant walk#16
Christian-Katzmann wants to merge 1 commit into
mainfrom
add-fixture-suite

Conversation

@Christian-Katzmann
Copy link
Copy Markdown
Owner

@Christian-Katzmann Christian-Katzmann commented Jun 2, 2026

What & why

app-it's core promise — double-click → server comes up on a free port → window shows the app → Cmd+Q kills everything → next click is fast — lives in deterministic, env-parameterized shell. Until now the macOS lane had only static CI checks (bash -n, plutil, swiftc -typecheck), while the Windows lane already did a functional round-trip. This adds the missing behavioral coverage and a rule to keep the support matrix honest.

What's in it

  • scripts/test-fixtures.sh + scripts/fixtures/ — a hermetic, CI-gated suite that drives the real scripts (inspect.sh, desktop-build.sh, the generated launcher, desktop-doctor.sh, desktop-quit.sh) against tiny project shapes (Vite, Next, static-export, Vite+Express multiserver, hardcoded-port, Chrome-fallback, deep-tree) and asserts the headless-automatable rows of SKILL.md's Phase‑4 checklist: build, bundle metadata, no placeholder leak, runtime port, server responds, server-belongs-to-the-launcher (descendant-walk ownership), warm-reattach, and clean teardown.
  • Hermetic by design — stand-in $PORT servers (scripts/lib/), no framework installs, a sandboxed HOME, trap-based teardown — so it never touches your real ~/Applications / ~/Library state. A weekly fixtures-real CI lane runs one real npm run dev -- --port $PORT Vite app so framework drift can't silently rot the top recipe.
  • APP_IT_SMOKE seam in the launcher templates — does everything a real Dock click does except open the GUI window, so CI (and SSH debugging) can verify the server comes up without a display. Inert on a normal launch.
  • Recipe-governance rule in CONTRIBUTING.md: no new framework recipe merges without a fixture or reproducible smoke test (directly governs the open Vite/Svelte/Astro recipe issues).
  • GUI-only checks (window content, Dock icon, Cmd+Q/red‑X, LaunchServices) stay a documented manual release smoke in docs/RELEASE_CHECKLIST.md.

Bug the suite caught on day one (and this PR fixes)

On first contact with a real framework, the suite surfaced a genuine pre-existing bug: the process-ownership descendant walk (the reattach gate in every run-template*.sh, and desktop:doctor) only reached the first child generation on macOS — pgrep -P returns nothing when handed a space-joined PID list, which is what the walk built after generation 1. So for the common case where the dev server's HTTP listener sits deeper than one level (npm run dev → node → vite; pnpm dev → node → node → next-server), warm-reattach silently failed: every Dock click cold-started a new server on a new port instead of reusing the warm one. The walk now expands one PID per call; the deep-tree fixture guards against regression.

Test plan

  • ./scripts/validate.sh — passes (static gate; also bash-n's the new scripts).
  • ./scripts/test-fixtures.sh92/92, hermetic, leaves no processes/ports/temp dirs.
  • APP_IT_RUN_REAL=1 ./scripts/test-fixtures.sh — real-Vite lane passes (npm install + real launch + reattach).
  • Verified self-contained in a clean clone at this commit (CI-equivalent): validate.sh passes with no desktop-icons-preview coupling.

Scope note

This PR is intentionally limited to the fixture suite + the walk fix + wiring. An in-flight icon-preview feature on the same working tree was deliberately left out so the two land independently.

Summary by Sourcery

Add a macOS behavioral fixture suite for app-it and fix process descendant-walk logic so warm reattach works reliably, along with wiring it into CI and documentation.

New Features:

  • Introduce a hermetic behavioral fixture suite driven by scripts/test-fixtures.sh and scripts/fixtures/ to exercise real launcher and build scripts across representative project shapes.
  • Add an APP_IT_SMOKE headless launch mode in macOS launcher templates to allow CI and headless environments to verify server startup without opening a GUI window.

Bug Fixes:

  • Correct macOS descendant process walking in launcher and desktop-doctor scripts by iterating pgrep per PID, ensuring warm-reattach and ownership checks reach deeper process trees.

Enhancements:

  • Update validate.sh to syntax-check the new fixture suite script and Node-based stub servers and to clarify its role as the fast static gate.
  • Document framework-recipe governance and fixture requirements in CONTRIBUTING.md and describe the fixture suite's role and limitations in scripts/fixtures/README.md.
  • Expand the release checklist with steps for running the behavioral fixture suite, a real-framework lane, and manual GUI smoke tests.

CI:

  • Extend the macOS CI workflow to run the behavioral fixture suite on every push and add a scheduled/dispatchable fixtures-real job that runs a real Vite-based fixture.

🤖 Generated with Claude Code

Add scripts/test-fixtures.sh + scripts/fixtures/ — a hermetic, CI-gated
suite that drives the real launcher scripts against tiny project shapes
(Vite, Next, static-export, Vite+Express, hardcoded-port, Chrome-fallback,
deep-tree) and asserts the headless-automatable rows of SKILL.md's Phase-4
checklist: build, bundle metadata, no placeholder leak, runtime port,
server responds, server-belongs-to-the-launcher (descendant-walk ownership),
warm-reattach, and clean teardown. Hermetic by design — stand-in $PORT
servers, no framework installs, a sandboxed HOME, trap-based teardown — so it
never touches the user's real ~/Applications / ~/Library state. A weekly
fixtures-real CI lane runs one real `npm run dev -- --port $PORT` Vite app so
framework drift can't silently rot the top recipe.

Add a documented APP_IT_SMOKE seam to the launcher templates so CI (and SSH
debugging) can verify the dev server comes up without opening the GUI window.

Fix a real bug the suite caught on first contact with a real framework: the
process-ownership descendant walk (the reattach gate in every run-template,
and desktop:doctor) only reached the first child generation on macOS, because
`pgrep -P` returns nothing for a space-joined PID list. Warm-reattach therefore
silently failed for npm/pnpm/Next/Vite (listener at gen 2+), cold-starting a
new server on a new port each click instead of reusing the warm one. The walk
now expands one PID per call; the deep-tree fixture guards against regression.

Adopt a recipe-governance rule in CONTRIBUTING: no new framework recipe merges
without a fixture or reproducible smoke test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jun 2, 2026

Reviewer's Guide

Adds a macOS behavioral fixture test suite that drives the real app-it launcher scripts against hermetic project fixtures, introduces a headless APP_IT_SMOKE launch seam, fixes the macOS descendant process walk so warm-reattach and ownership checks work across multi-generation trees, and wires these into CI, docs, and validation tooling.

Sequence diagram for the updated descendant walk warm-reattach logic

sequenceDiagram
    participant Launcher_run_template_sh as run-template.sh
    participant pgrep
    participant DevServer

    Launcher_run_template_sh->>DevServer: start dev server (supervisor PID = EXPECTED_PID)
    DevServer-->>Launcher_run_template_sh: writes PORT_FILE, PID_FILE

    loop up to 4 generations
        Launcher_run_template_sh->>pgrep: pgrep -P EXPECTED_PID
        pgrep-->>Launcher_run_template_sh: child PIDs (gen 1)
        Launcher_run_template_sh->>pgrep: pgrep -P each_child_PID (one PID per call)
        pgrep-->>Launcher_run_template_sh: deeper descendants (gen 2+)
    end

    Launcher_run_template_sh->>Launcher_run_template_sh: check if runtime-port PID is in DESCENDANTS
    alt PID in DESCENDANTS
        Launcher_run_template_sh-->>Launcher_run_template_sh: warm reattach to existing DevServer
    else PID not in DESCENDANTS
        Launcher_run_template_sh-->>Launcher_run_template_sh: start new server on new port
    end
Loading

Sequence diagram for APP_IT_SMOKE headless launch in the fixture suite

sequenceDiagram
    actor CI as test-fixtures.sh
    participant Launcher_run_template_sh as run-template.sh
    participant StubServer as stub-server.js

    CI->>Launcher_run_template_sh: APP_IT_SMOKE=1 ./run (bundle run)
    Launcher_run_template_sh->>StubServer: start dev server (daemonized)
    StubServer-->>Launcher_run_template_sh: listening on chosen PORT

    Launcher_run_template_sh-->>Launcher_run_template_sh: write PID_FILE, PORT_FILE
    Launcher_run_template_sh-->>CI: print URL and PID (app-it smoke: ...)
    Launcher_run_template_sh-->>Launcher_run_template_sh: exit 0 (no GUI wrapper)

    CI->>StubServer: curl http://localhost:$PORT
    StubServer-->>CI: 200 OK

    CI->>Launcher_run_template_sh: desktop-quit.sh (stop server)
    Launcher_run_template_sh-->>StubServer: terminate DevServer
    StubServer-->>CI: process exits, port freed
Loading

File-Level Changes

Change Details Files
Fix descendant process walk on macOS so warm-reattach and ownership checks follow multi-generation trees instead of stopping at first children.
  • Replace single pgrep -P over a space-joined PID list with a per-PID loop that aggregates child PIDs for each generation.
  • Update run templates for app-it, app-it-static, multiserver, and Chrome fallback launchers to use the new descendant expansion logic and more robust empty-generation checks.
  • Align desktop-doctor.sh's walk_descendants helper with the new per-PID generation walk so diagnostics use the same ownership semantics as launchers.
plugins/app-it/skills/app-it/templates/run-template.sh
plugins/app-it-static/skills/app-it-static/templates/run-template-static-server.sh
plugins/app-it/skills/app-it/templates/run-template-multiserver.sh
plugins/app-it/skills/app-it/templates/run-template-chrome.sh
plugins/app-it/skills/app-it/templates/desktop-doctor.sh
Introduce a headless APP_IT_SMOKE launch seam so CI and SSH runs can fully start servers without opening GUI windows.
  • Add an APP_IT_SMOKE environment flag check in run templates that, after successfully starting and recording server PIDs and ports, prints runtime URLs and exits 0 instead of exec-ing the GUI wrapper.
  • Implement variant messaging for single-server, static-server, and multiserver templates to include relevant PIDs and ports.
  • Ensure the smoke path has no effect on normal Dock launches when APP_IT_SMOKE is unset.
plugins/app-it/skills/app-it/templates/run-template.sh
plugins/app-it-static/skills/app-it-static/templates/run-template-static-server.sh
plugins/app-it/skills/app-it/templates/run-template-multiserver.sh
Add a hermetic behavioral fixture suite that drives the real scripts against small project shapes and asserts lifecycle guarantees.
  • Create scripts/test-fixtures.sh to orchestrate sandboxed HOME, temporary work trees, build and run flows, and assertions across fixtures, including server ownership, port usage, warm-reattach, and teardown.
  • Implement helper Node-based stub-server.js and stub-nested.js to simulate PORT-honoring dev servers and deeper process trees without installing real frameworks.
  • Define multiple fixture project shapes (Vite, Next, static export, multiserver, hardcoded-port, Chrome fallback, deep-tree, vite-real) with minimal package.json/config/HTML assets and per-fixture expectations documented in scripts/fixtures/README.md.
scripts/test-fixtures.sh
scripts/lib/stub-server.js
scripts/lib/stub-nested.js
scripts/fixtures/README.md
scripts/fixtures/vite-basic/package.json
scripts/fixtures/vite-basic/index.html
scripts/fixtures/vite-basic/vite.config.ts
scripts/fixtures/vite-express/package.json
scripts/fixtures/vite-express/vite.config.ts
scripts/fixtures/vite-express/server/index.js
scripts/fixtures/next-basic/package.json
scripts/fixtures/next-basic/next.config.js
scripts/fixtures/next-basic/app/page.tsx
scripts/fixtures/static-export/package.json
scripts/fixtures/static-export/next.config.js
scripts/fixtures/static-export/out/index.html
scripts/fixtures/deep-tree/package.json
scripts/fixtures/hardcoded-port/package.json
scripts/fixtures/hardcoded-port/vite.config.ts
scripts/fixtures/chrome-fallback/package.json
scripts/fixtures/chrome-fallback/index.html
scripts/fixtures/chrome-fallback/vite.config.ts
scripts/fixtures/vite-real/package.json
scripts/fixtures/vite-real/index.html
scripts/fixtures/vite-real/vite.config.ts
scripts/fixtures/vite-real/src/main.js
scripts/fixtures/chrome-fallback/app-it.config.json
scripts/fixtures/deep-tree/app-it.config.json
scripts/fixtures/hardcoded-port/app-it.config.json
scripts/fixtures/next-basic/app-it.config.json
scripts/fixtures/static-export/app-it.config.json
scripts/fixtures/vite-basic/app-it.config.json
scripts/fixtures/vite-express/app-it.config.json
scripts/fixtures/vite-real/app-it.config.json
Wire the behavioral fixture suite and Node helpers into validation, CI, and contributor/release documentation, and add recipe-governance rules.
  • Update scripts/validate.sh to bash -n the new test-fixtures.sh and, when Node is available, syntax-check scripts/lib/*.js, plus clarify messaging about the behavioral gate.
  • Extend GitHub Actions CI to run scripts/test-fixtures.sh on every macOS validate job and add a scheduled or manually-triggered fixtures-real job that runs APP_IT_RUN_REAL=1 ./scripts/test-fixtures.sh.
  • Document the fixture suite and new expectations in CONTRIBUTING.md, docs/RELEASE_CHECKLIST.md, and scripts/fixtures/README.md, and add corresponding Unreleased notes in CHANGELOG.md, including the rule that new framework recipes require fixtures or smoke tests.
scripts/validate.sh
.github/workflows/ci.yml
CONTRIBUTING.md
docs/RELEASE_CHECKLIST.md
scripts/fixtures/README.md
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The descendant-walk logic for process ownership is now implemented in multiple places (launcher templates, desktop-doctor.sh, test-fixtures.sh); consider extracting this into a single shared helper so future fixes to the walk don’t risk drifting across copies.
  • scripts/test-fixtures.sh assumes availability of several external tools (lsof, curl, PlistBuddy, npm/node, etc.); adding an upfront preflight check with clear error messages for missing dependencies would make failures easier to interpret and avoid partial runs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The descendant-walk logic for process ownership is now implemented in multiple places (launcher templates, desktop-doctor.sh, test-fixtures.sh); consider extracting this into a single shared helper so future fixes to the walk don’t risk drifting across copies.
- scripts/test-fixtures.sh assumes availability of several external tools (lsof, curl, PlistBuddy, npm/node, etc.); adding an upfront preflight check with clear error messages for missing dependencies would make failures easier to interpret and avoid partial runs.

## Individual Comments

### Comment 1
<location path="scripts/lib/stub-nested.js" line_range="27" />
<code_context>
+  console.error(`stub-nested: failed to spawn node: ${err.message}`);
+  process.exit(1);
+});
+child.on('exit', (code) => process.exit(code == null ? 0 : code));
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Propagating a `null` exit code from the child as 0 may mask signal-based failures.

When the child exits due to a signal, Node sets `code` to `null` and passes the reason via the `signal` argument. Converting `null` to `0` makes signal-based terminations (e.g. SIGTERM/SIGKILL) look like a successful exit, hiding underlying launcher/fixture failures. Consider instead treating `null` as a non-zero exit, or inspecting the `signal` and mapping it to an appropriate non-zero code so that unexpected terminations are not reported as success.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

console.error(`stub-nested: failed to spawn node: ${err.message}`);
process.exit(1);
});
child.on('exit', (code) => process.exit(code == null ? 0 : code));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Propagating a null exit code from the child as 0 may mask signal-based failures.

When the child exits due to a signal, Node sets code to null and passes the reason via the signal argument. Converting null to 0 makes signal-based terminations (e.g. SIGTERM/SIGKILL) look like a successful exit, hiding underlying launcher/fixture failures. Consider instead treating null as a non-zero exit, or inspecting the signal and mapping it to an appropriate non-zero code so that unexpected terminations are not reported as success.

@Christian-Katzmann Christian-Katzmann deleted the add-fixture-suite branch June 2, 2026 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant