Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/skills/write-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: write-tests
description: Write meaningful Replica tests. Use when adding regression coverage, testing new behavior, or changing existing automated tests.
---

# Writing meaningful tests

## Establish the contract

Read the [root instructions](../../../AGENTS.md), [shared coding conventions](../../../docs/coding-conventions.md), and [testing and review guide](../../../docs/testing-and-review.md), then the affected project's instructions and relevant guides. The shared guide owns test-quality criteria and the independent-writer policy.

Identify the accepted behavior, public interface, and existing coverage. For each proposed case, state the distinct regression it catches and why existing cases would miss it. Drop redundant cases and incidental assertions. Complete this step when every retained case has an expected outcome grounded in requirements rather than current implementation.

## Choose the writer

Apply the shared guide's independent-writer policy. For substantial behavior changes and nontrivial bug fixes, delegate once to an available code-capable agent with permission to edit tests. Use a synchronous task unless implementation can safely proceed in separate files while the writer works.

If you are already the assigned test writer, write the tests directly; this assignment is the independent pass.

Give the writer a self-contained assignment containing:

- The relevant user request and accepted requirements, separately from implementation suggestions.
- The bug reproduction or behavior to add, including known failure and boundary cases.
- Affected interfaces and paths, existing coverage, project guidance, and any implementation already completed.
- Ownership of named test files and necessary test fixtures; production changes remain with the implementation agent.
- A bounded result: implement the smallest distinct coverage, run focused checks, and return changed files, the regression each case catches, observed red/green evidence, and unresolved requirements or production defects.

When behavior is unclear, resolve it with the user before asking the writer to encode it. Small changes follow the same process directly without delegation.

## Write and verify

Use the existing runner and repository test patterns. Establish a meaningful failing case where feasible, then run the smallest command covering the completed change. Preserve unrelated work when checking pre-fix behavior; prefer an isolated reproduction over reverting a dirty worktree.

Review the resulting tests against the shared criteria. Ensure mocks leave the behavior under test real, expectations come from the contract, and every added case protects a distinct regression. Resolve reported production defects in the implementation agent, then rerun affected tests and follow the project's final-check requirements.

Finish only when the retained cases prove their intended outcomes, or explicitly report the blocker and unverified behavior. Distinguish observed results from expected coverage, and maintenance simplification from measured runtime savings. Keep the handoff concise; the per-case rationale belongs in the writer's working result, not boilerplate code comments or a new tracking document.
26 changes: 25 additions & 1 deletion .github/workflows/actions-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ permissions:
contents: read

jobs:
changes:
uses: ./.github/workflows/ci-changes.yml

test:
name: Replica action tests
name: Build action suite
needs: changes
if: ${{ needs.changes.outputs.build_action == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -28,3 +33,22 @@ jobs:
- name: Prepare composite action fixture
run: cp actions/build/test/fixture/package.json actions/build/test/fixture/package-lock.json actions/build/test/fixture/fixture-build.mjs .
- uses: ./actions/build

result:
name: Replica action tests
needs: [changes, test]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Require the selected build-action suite to pass
env:
SELECTION_RESULT: ${{ needs.changes.result }}
SELECTED: ${{ needs.changes.outputs.build_action }}
TEST_RESULT: ${{ needs.test.result }}
run: |
test "$SELECTION_RESULT" = success
case "$SELECTED" in
true) test "$TEST_RESULT" = success ;;
false) test "$TEST_RESULT" = skipped ;;
*) echo "Invalid build-action suite selection" >&2; exit 1 ;;
esac
40 changes: 40 additions & 0 deletions .github/workflows/ci-changes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI selection

on:
workflow_call:
outputs:
hub:
description: Run the complete Hub suite
value: ${{ jobs.changes.outputs.hub }}
build_action:
description: Run the build-action suite
value: ${{ jobs.changes.outputs.build_action }}

permissions:
contents: read

jobs:
changes:
name: Select affected suites
runs-on: ubuntu-latest
outputs:
hub: ${{ steps.select.outputs.hub }}
build_action: ${{ steps.select.outputs.build_action }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
- uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
with:
working-directory: hub
- run: node --test scripts/select-ci-suites.test.mjs
- run: ruby scripts/check-ci-workflows.rb
- id: select
run: node scripts/select-ci-suites.mjs
env:
CI_EVENT_NAME: ${{ github.event_name }}
CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CI_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
58 changes: 56 additions & 2 deletions .github/workflows/hub-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ defaults:
working-directory: hub

jobs:
changes:
uses: ./.github/workflows/ci-changes.yml

rails_test:
name: Rails tests
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -45,6 +50,8 @@ jobs:

development_queue:
name: Development queue
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -57,6 +64,8 @@ jobs:

ruby_lint:
name: Ruby lint
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
env:
RUBOCOP_CACHE_ROOT: tmp/rubocop
Expand All @@ -70,6 +79,8 @@ jobs:

zeitwerk:
name: Zeitwerk
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -82,6 +93,8 @@ jobs:

annotations:
name: Annotations
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -94,6 +107,8 @@ jobs:

bundler_audit:
name: Bundler audit
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -105,6 +120,8 @@ jobs:

typecheck:
name: TypeScript types
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -118,6 +135,8 @@ jobs:

frontend_lint:
name: Frontend lint
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -131,23 +150,42 @@ jobs:

frontend_test_shard:
name: Frontend tests shard ${{ matrix.shard }}/4
needs: changes
if: ${{ always() }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Require valid suite selection
working-directory: .
env:
SELECTION_RESULT: ${{ needs.changes.result }}
SELECTED: ${{ needs.changes.outputs.hub }}
run: |
test "$SELECTION_RESULT" = success
case "$SELECTED" in
true|false) ;;
*) echo "Invalid Hub suite selection" >&2; exit 1 ;;
esac
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: ${{ needs.changes.outputs.hub == 'true' }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: ${{ needs.changes.outputs.hub == 'true' }}
with:
node-version-file: hub/package.json
cache: npm
cache-dependency-path: hub/package-lock.json
- run: npm ci
if: ${{ needs.changes.outputs.hub == 'true' }}
- run: npm test -- --shard=${{ matrix.shard }}/4
if: ${{ needs.changes.outputs.hub == 'true' }}

frontend_integration_test:
name: Controls and extension tests
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -169,19 +207,29 @@ jobs:

frontend_test:
name: Frontend tests
needs: [frontend_test_shard, frontend_integration_test]
needs: [changes, frontend_test_shard, frontend_integration_test]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Require all frontend suites to pass
working-directory: .
env:
SELECTION_RESULT: ${{ needs.changes.result }}
SELECTED: ${{ needs.changes.outputs.hub }}
SHARD_RESULT: ${{ needs.frontend_test_shard.result }}
INTEGRATION_RESULT: ${{ needs.frontend_integration_test.result }}
run: test "$SHARD_RESULT" = success && test "$INTEGRATION_RESULT" = success
run: |
test "$SELECTION_RESULT" = success
case "$SELECTED" in
true) test "$SHARD_RESULT" = success && test "$INTEGRATION_RESULT" = success ;;
false) test "$SHARD_RESULT" = success && test "$INTEGRATION_RESULT" = skipped ;;
*) echo "Invalid Hub suite selection" >&2; exit 1 ;;
esac

frontend_build:
name: Frontend build
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -197,6 +245,8 @@ jobs:

storybook_build:
name: Storybook build
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -210,6 +260,8 @@ jobs:

production_database:
name: Production database
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -224,6 +276,8 @@ jobs:

production_image:
name: Production image
needs: changes
if: ${{ needs.changes.outputs.hub == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Read [Contributing](CONTRIBUTING.md) and the [shared documentation index](docs/R
- [Coding conventions](docs/coding-conventions.md) apply to complete changes, tests, and comments.
- [Documentation conventions](docs/documentation-conventions.md) apply to discussions, prose, labels, and errors.
- [Testing and review](docs/testing-and-review.md) explains how to choose checks and review a change.
- Use the [test-writing skill](.github/skills/write-tests/SKILL.md) when adding or changing automated tests.

Keep shared rules in root guides and project-specific rules in the affected project. Do not apply Hub's Rails, Primer, or devcontainer requirements to the starter, build action, or shared packages. The starter's copied instructions must remain usable without this monorepo.

Expand Down
42 changes: 42 additions & 0 deletions docs/testing-and-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,54 @@ Use existing commands and inspect their configuration. Do not invent commands or

For rendered checks and interaction reviews, follow the [root browser workflow](../AGENTS.md#browser-validation-and-playwright-fallback). Existing automated suites, CI browser integration, and build-artifact capture retain their configured drivers.

## Write meaningful tests

Use the [test-writing skill](../.github/skills/write-tests/SKILL.md) when adding or changing automated tests. Apply these criteria when reviewing existing coverage too.

Start with the intended behavior and the regression a test should catch. Inspect existing coverage before adding cases. Each case should protect a distinct outcome, boundary, or failure mode through the smallest useful public interface. An existing case that already catches the regression usually needs an update rather than another test.

Choose assertions that survive an equivalent implementation. Check outcomes such as saved state, returned data, navigation, access decisions, and usable controls. Exact protocol values, serialization, accessible names, meaningful state mappings, and safety restrictions can be contracts worth asserting. Ordinary copy, icon counts, internal markup, and whole configuration snapshots usually are not. A text query that identifies a control is different from a test whose only purpose is freezing its wording. For agent instructions, distinguish discoverable metadata and required safety guidance from incidental phrasing; phrase presence alone does not prove agent behavior.

Keep each behavior at its owning layer and use integration tests to prove important connections. Repeating all lower-level cases through every consumer adds cost without necessarily adding confidence. Keep separate cases for distinct failures; combine assertions when they describe one coherent scenario. Parameterization improves readability but still runs every input. Fewer case names or assertions do not establish a faster suite.

For a bug fix, reproduce the failure before the fix when feasible. For new behavior, establish an expected-result failure before implementation when the interface allows it. A missing dependency, syntax error, or test setup failure is not evidence that the test catches the regression. If the implementation already exists, explain the plausible incorrect behavior the test would reject; report red-phase evidence as unavailable unless it was actually observed.

### Independent test writing

Use one separate test-writing agent for substantial behavior changes and nontrivial bug fixes. These include changes spanning multiple runtime layers, access or persistence rules, concurrent state changes, or a regression requiring non-obvious reproduction. Keep small, local changes in the implementation agent; a separate agent's context and execution cost should earn distinct coverage.

Give the test writer the accepted requirements, relevant issue or bug reproduction, public interfaces, affected paths, existing coverage, and project instructions. Identify unresolved behavior instead of letting the writer infer requirements from current output. The writer should derive expected outcomes from that contract, then inspect implementation as needed to use the real interfaces. Separate context reduces shared assumptions; it does not guarantee independent reasoning.

Assign test files to the writer and production files to the implementation agent. The writer may update test fixtures needed for its cases, but reports production defects and ambiguous requirements instead of changing production behavior or weakening expectations to pass. Finish with one integration review of the combined change. If a separate agent is unavailable, apply the same contract-first process directly and disclose that the independent pass did not happen.

## Keep local checks and CI aligned

Project commands define the check. GitHub Actions should prepare the required environment and call the same command rather than recreate its behavior.

When a command gains a dependency or build prerequisite, update its local setup and affected workflow in the same change. A green local suite does not prove that CI installed the same dependencies, so inspect both paths.

### Select affected CI suites

Hub and build-action workflows start on every pull request, but run their expensive jobs only when the change can affect that suite. Pushes to `main` run both complete suites. Local project commands still run their complete checks; selection changes when CI calls them, not what they do. Dependency-age checks remain unconditional, and Starter and Learning CI retain their own path filters.

The [shared selection workflow](../.github/workflows/ci-changes.yml) calls [the selector](../scripts/select-ci-suites.mjs). For pull requests, it compares the merge base with the PR head using Git rather than the size-limited changed-files API. Deletions and both sides of renames count as changes.

| Changed area | Selected suites |
| --- | --- |
| Hub code and configuration | Hub |
| Shared Controls runtime, schema, fixtures, and capture adapter under `hub/app/javascript/utils/prototype-controls/` | Hub and build action |
| `actions/build/` | Hub and build action; Hub uses the action for local packaging and browser checks |
| Copilot extension and plugin catalog | Hub |
| `packages/learning/` | Hub; its image, lint, and learning integration consume the package |
| `starter/` | Hub; local setup copies the starter and depends on its router, metadata, dependencies, and setup files; the Controls runtime suite also checks its shared reference. Starter CI still runs separately |
| Existing installer scripts | Neither; Starter CI covers them |
| Root contributor documents, shared and Hub guides, skills, and documentation assets | Neither |
| Workflow changes, selector changes, and unclassified paths | Both |

The workflows preserve the existing required check names; skipping an entire workflow with a path filter could leave those checks pending. Static jobs use job-level conditions. The four required Vitest shard jobs always expand their matrix and validate selection, but skip checkout, setup, and tests when Hub is unaffected. The required `Frontend tests` and `Replica action tests` result jobs accept omitted work only after successful selection explicitly excluded it. Selection failures, invalid outputs, and failures in selected suites fail those result checks. Keep required-check configuration unchanged.

When adding a shared dependency or a new project, update the selector and its behavioral tests together. Unclassified paths deliberately run both suites until their dependencies are known. Run `node --test scripts/select-ci-suites.test.mjs` and `ruby scripts/check-ci-workflows.rb` from the repository root. The latter reads the actual workflow gates and checks their success, failure, cancellation, and missing-output behavior; both commands run before CI selects suites. Check changed workflows with `actionlint` when available. Actual job skipping and check reporting still need verification on GitHub after publishing.

## Update dependencies

Hub and the shared build action wait seven days before resolving newly published dependencies. npm enforces the delay through each project's committed `.npmrc`. Hub's RubyGems source in `Gemfile` applies the delay to Bundler. Dependabot applies the same cooldown to npm, Bundler, and GitHub Actions updates across the repository. The starter manages its dependency resolution independently.
Expand Down
Loading
Loading