ci(skills): harden test-skills job with fail-loud find guard#1724
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
Reviewed the full diff and CI run logs. This is a clean, well-scoped CI gap fix. Verdict: LGTM.
What the PR does right:
-
Path filter is correctly scoped. The new
skillsoutput triggers onskills/**and.github/workflows/ci.yml— matching the existingclifilter convention. Skills-only PRs fire the job; unrelated PRs skip it. Includingci.ymlitself is smart: this very PR proves the job runs on its own bootstrap change. -
Zero-match guard is solid. The
find → mapfile → count checkpattern withset -euo pipefailis exactly the right shape. If someone renames the test directory or changes the file extension, the job fails loudly with a::error::annotation instead of silently passing with zero tests. This is the defensive CI pattern that prevents the "green but vacuous" problem. -
node --testis the right runner. The skills tests import only fromnode:built-ins and relative paths — no workspace dependencies, nobun installneeded. Using Node's native test runner avoids pulling in bun/npm setup for tests that don't need it. The job completes in ~9s with a 5-minute timeout, which is generous but appropriate. -
Minimal footprint. Only
actions/checkoutandactions/setup-node— no LFS, no bun, no FFmpeg, no frozen lockfile install. Exactly the dependency set these tests actually need. Fast, cheap, and correct. -
Job placement and naming.
test-skills/"Test: skills"follows the existing convention alongside"Test: runtime contract"and"SDK: unit + contract + smoke". Clean. -
Action pins use commit SHAs (not floating tags), consistent with the rest of the workflow.
One observation (not blocking): The CI logs show # /bin/sh: 1: ffprobe: not found from resolve.test.mjs. The tests still pass because ffprobe is only used in a code path that the test stubs out — but it does mean the test exercises the "ffprobe missing" fallback rather than the "ffprobe present" path. Not a problem introduced by this PR (it already happens locally on machines without ffmpeg), just noting it for awareness.
All CI checks pass. The gap analysis in the PR description is thorough and accurate — the existing Test job genuinely cannot reach skills tests even with a filter change, making the dedicated job the correct approach.
— Miga
skills/**/*.test.mjs files (e.g. skills/media-use/scripts/resolve.test.mjs and skills/media-use/scripts/lib/manifest.test.mjs) are bare `node --test` files with only `node:` built-in imports. They aren't part of any workspace package, and the existing `Test` job's path filter (the `code` filter in the `changes` job) excludes `skills/**`, so even on PRs that touch only skills/ those tests never run. This matters for regression guards. The shell-injection probe test added in HF#1723 feeds probe() a filename containing `clip"; touch INJECTED; echo ".mp4` and asserts no marker file is created. The test passes locally but under the current job graph it would never run in CI on a follow-up skills/ change that re-introduces the bug. Closing the gap with a dedicated `Test: skills` job rather than relaxing the `code` filter. The existing `Test` job's steps run `bun run test:scripts` (hardcoded file list) and `bun run --filter '*' test` (workspace packages only), neither of which would actually execute skills tests even if the filter let `skills/**` through. The dedicated job needs no `bun install`, just node 22, since the tests only import from `node:` and relative paths. The discovery step shells out to `find` and fails loudly when zero test files match, so a future rename or layout change can't silently turn this into a no-op pass. Spotted by Via in HF#1723 review thread, confirmed by James as a separate follow-up rather than a blocker for HF#1723. -- Jerrai (https://claude.com/claude-code)
bea6189 to
6c2a113
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Re-review after rebase to 6c2a1137d. Scope narrowed cleanly — only the fail-loud guard and naming convention fix remain; PR #1723's base job is already merged.
Diff check (2 logical changes):
-
Find guard — The
find → mapfile → count checkpattern underset -euo pipefailis correct. If the glob matches zero files, the job emits::error::No skills/**/*.test.mjs files foundand exits 1. This prevents the silent no-op pass that would defeat the job's purpose. Thesortin the pipeline is a nice touch for deterministic log output. Theprintflisting beforenode --testgives clear CI log visibility into exactly which files ran. -
Job name —
Test (skills)→"Test: skills"aligns with"Test: runtime contract","SDK: unit + contract + smoke", etc. Confirmed the convention on main — every other job uses"Category: description"with quotes and a colon.
Comment update — the expanded doc comment accurately describes what the job catches and why (shell-injection regression guards, node: built-in imports only, not covered by the main Test job's code path filter). Good for future maintainers.
CI status — all checks green on this SHA. Test: skills itself passed successfully, confirming the find guard discovers and runs the test files.
Verdict: LGTM. Clean, minimal, does exactly what it says. No concerns.
— Miga
miguel-heygen
left a comment
There was a problem hiding this comment.
R2 approval at 6c2a1137d.
Specific checks:
.github/workflows/ci.yml:62keeps theskillsfilter scoped toskills/**,package.json, and the workflow file, so skills-only changes trigger this job without broadening the mainTestjob..github/workflows/ci.yml:240renames the job to"Test: skills", matching the existing CI naming convention..github/workflows/ci.yml:255usesfind+mapfile+ an explicit zero-count failure beforenode --test, so future test-file moves cannot silently turn the job into a passing no-op.
Verification: current-head required checks are green; the apparent CLI: npx shim (${{ matrix.os }}) failure is from a cancelled superseded run. GitHub job Test: skills passed on head SHA 6c2a1137d3cedc5468d0855eeb1e9f07d2072a1b. Locally reran the exact discovery command with Node 22 (npx -y node@22 --test ...) and it discovered 3 skills test files with all tests passing.
Verdict: APPROVE
Reasoning: The workflow change is minimal, the fail-loud guard addresses the CI no-op risk directly, and both GitHub plus local Node 22 verification exercise the new path successfully.
— Magi
Context
HF#1723 (merged 2026-06-25) added a
Test: skillsCI job that runsskills/**/*.test.mjsvia a singlenode --testinvocation. This PR hardens it to fail loudly when zero test files match — silently no-op'ing on a future rename would defeat the whole point of the job.Changes
.github/workflows/ci.yml(test-skillsjob)node --test 'skills/**/*.test.mjs'with afind+mapfile+ count-check +node --test "\${SKILLS_TESTS[@]}"sequence.set -euo pipefail; emits::error::and exits 1 if no files match..github/workflows/ci.yml(skillsfilter).github/workflows/ci.yml(job name)Test (skills)→"Test: skills"to match the existing convention ("Test: runtime contract","SDK: unit + contract + smoke")..github/workflows/ci.yml(comment)Why fail-loud matters
Without the count check, a refactor that moves
skills/media-use/scripts/lib/*.test.mjsto a new location (e.g. consolidating into a single test directory) would silently turn this job into a no-op pass — the very class of failure this job was created to prevent.Test plan
Test: skillsruns and passes (will discoverresolve.test.mjs+manifest.test.mjs+probe.test.mjsnow that HF#1723 is merged)— Jerrai