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
37 changes: 34 additions & 3 deletions .claude/skills/add-rubyzen-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
---
name: add-rubyzen-tests
description: Write unit tests for Rubyzen's own API components — declarations, providers, collections, or matchers. Use this skill when the user wants to add tests for an existing or newly added Rubyzen component, increase test coverage, or write specs for untested methods. Also trigger when the user says "test this declaration", "add specs for", or "write tests for the X collection".
description: Write unit tests for Rubyzen's own API components — declarations, providers, collections, RSpec matchers, or Minitest assertions. Use this skill when the user wants to add tests for an existing or newly added Rubyzen component, increase test coverage, or write specs for untested methods. Also trigger when the user says "test this declaration", "add specs for", or "write tests for the X collection".
---

# Writing Unit Tests for Rubyzen

You are writing unit tests for Rubyzen's internal API — the declarations, providers, collections, and matchers that make up the library. These tests verify that Rubyzen's own code works correctly.
You are writing unit tests for Rubyzen's internal API — the declarations, providers, collections, RSpec matchers, and Minitest assertions that make up the library. These tests verify that Rubyzen's own code works correctly.

**This is NOT about writing lint rules.** Lint rules test user codebases; unit tests test Rubyzen itself. Use the `write-lint-rule` skill for lint rules.

Rubyzen has **two unit-test suites**: RSpec specs in `spec/` cover the core API (declarations, collections, providers) and the RSpec matchers; Minitest tests in `test/` cover the Minitest assertions (`assert_zen_*`).

## Step 0: Understand the Test Infrastructure

Read these files first:
Expand Down Expand Up @@ -225,6 +227,34 @@ end

**Note:** Do NOT use `fail_with` — it's not available. Use `raise_error(RSpec::Expectations::ExpectationNotMetError, /pattern/)` to test failure messages.

## Writing Minitest Assertion Tests

The Minitest assertions (`assert_zen_empty` / `assert_zen_true` / `assert_zen_false`) live in `lib/rubyzen/assertions/` and are tested with **Minitest**, not RSpec, under `test/` — the counterpart of the matcher specs above.

Files: `test/assertions/assert_<assertion_name>_test.rb`

You need to require `test/test_helper.rb` (which requires `rubyzen/minitest` and provides the same `parse_ruby` helper via the `ParseHelper` module), subclass `Minitest::Test`, and use `assert_raises(Minitest::Assertion)` to test failures:

```ruby
require_relative '../test_helper'

class AssertZenEmptyTest < Minitest::Test
include ParseHelper

def test_zen_empty_passes_when_collection_is_empty
assert_zen_empty(Rubyzen::Collections::ClassesCollection.new)
end

def test_zen_empty_fails_when_collection_is_not_empty
collection = parse_ruby('class Foo; end').classes
error = assert_raises(Minitest::Assertion) { assert_zen_empty(collection) }
assert_match(/Expected to be empty/, error.message)
end
end
```

Run with `bundle exec rake test` (or `bundle exec ruby -Itest test/assertions/assert_zen_empty_test.rb`). The assertion tests cover the same cases as the matcher specs — pass/fail, `allowlist:`, `baseline:`, stale entries, custom `message:` — plus a missing block raising `ArgumentError` for `assert_zen_true` / `assert_zen_false`.

## When to Use Fixture Files

Use `spec/fixtures/` with real `.rb` files only when testing:
Expand All @@ -251,4 +281,5 @@ For everything else, use inline `parse_ruby` snippets.
- [ ] Tests type preservation for collection `filter`
- [ ] Snippets have 2+ statements when testing file-level providers
- [ ] Does NOT use `fail_with` (uses `raise_error` instead)
- [ ] `bundle exec rspec spec/` passes
- [ ] Minitest assertion tests live in `test/`, require `test_helper`, and use `assert_raises(Minitest::Assertion)`
- [ ] `bundle exec rake` passes (RSpec `spec/` + Minitest `test/`)
12 changes: 6 additions & 6 deletions .claude/skills/expand-rubyzen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ module Rubyzen
# decl.name #=> "example"
#
class <Concept>Declaration
# Always include these three for matcher output:
# Always include these three for matcher/assertion failure output:
include Rubyzen::Providers::FilePathProvider
include Rubyzen::Providers::LineNumberProvider
include Rubyzen::Providers::ClassNameProvider
Expand Down Expand Up @@ -82,7 +82,7 @@ module Rubyzen
# so if you use a custom name, add `alias :parent :<custom_name>` to ensure
# FilePathProvider can walk the tree.

# REQUIRED: Used by matchers for failure messages.
# REQUIRED: Used by the RSpec matchers and Minitest assertions for failure messages.
# @return [String]
def name
# Return a meaningful identifier for this declaration
Expand All @@ -97,8 +97,8 @@ end

**Rules:**
- `node` and `parent` are always `attr_reader` — providers depend on them
- `FilePathProvider`, `LineNumberProvider`, `ClassNameProvider` are **required** — matchers use `file_path`, `line`, and `class_name` for failure messages
- A `name` method is **required** — matchers call it via `element_name`
- `FilePathProvider`, `LineNumberProvider`, `ClassNameProvider` are **required** — the RSpec matchers and Minitest assertions use `file_path`, `line`, and `class_name` (via the shared `ExpectationHelpers`) for failure messages
- A `name` method is **required** — both frameworks call it via `element_name`
- Zeitwerk autoloads — no `require` statements needed (exception: if the file is loaded before Zeitwerk, add explicit `require_relative`)

## Step 3: Create the Provider
Expand Down Expand Up @@ -289,7 +289,7 @@ This affects any provider that uses `each_descendant` on file-level nodes.

1. Add the new declaration to the **Declaration Reference** table in `CLAUDE.md`
2. Add the new collection to the **Data Flow** tree in `CLAUDE.md`
3. Run `bundle exec rspec spec/` to verify all tests pass
3. Run `bundle exec rake` to verify all tests pass (RSpec `spec/` + Minitest `test/`)

## Checklist

Expand All @@ -308,4 +308,4 @@ Before considering the work complete, verify:
- [ ] Collection spec tests `CollectionFilterProvider` methods and domain-specific filters
- [ ] Test snippets have 2+ statements (single-statement gotcha)
- [ ] `CLAUDE.md` updated
- [ ] `bundle exec rspec spec/` passes
- [ ] `bundle exec rake` passes (RSpec `spec/` + Minitest `test/`)
39 changes: 32 additions & 7 deletions .claude/skills/run-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Run the sample project lint rules to verify that Rubyzen detects ar

The sample project (`sample_project/`) contains intentional architectural violations. Lint rule specs verify that Rubyzen correctly detects them.

## Run All Lint Rules
## Run RSpec Lint Rules

```bash
bundle exec rspec sample_project/spec/
Expand All @@ -21,6 +21,21 @@ bundle exec rspec sample_project/spec/
bundle exec rspec sample_project/spec/controllers/no_if_statements_in_controllers_lint_spec.rb
```

## Run Minitest Lint Rules

The sample project also has a parallel Minitest suite under `sample_project/test/` (using the `assert_zen_*` assertions). Run it with plain Ruby:

```bash
# All Minitest lint rules — single process, one aggregated summary
# (mirrors `bundle exec rspec sample_project/spec/`)
cd sample_project && bundle exec ruby -Itest -e 'Dir.glob("test/**/*_test.rb").sort.each { |f| require File.expand_path(f) }'

# A specific Minitest lint rule
cd sample_project && bundle exec ruby -Itest test/controllers/no_if_statements_in_controllers_test.rb
```

**Expected behavior:** same as RSpec — these are **expected to fail**, detecting the sample project's intentional violations.

## Lint Rule Structure

```
Expand All @@ -34,22 +49,32 @@ sample_project/
│ ├── requests/
│ ├── tests/
│ └── config.rb
└── spec/ # Lint rules as RSpec tests
├── spec_helper.rb # Shared context with collection helpers
├── spec/ # Lint rules as RSpec tests
│ ├── spec_helper.rb # Shared context with collection helpers
│ ├── controllers/
│ ├── models/
│ ├── presenters/
│ ├── tests/
│ └── ...
└── test/ # Lint rules as Minitest tests (parallel subset)
├── test_helper.rb # LintTestCase base class with collection accessors
├── controllers/
├── models/
├── presenters/
├── tests/
└── ...
├── repos/
└── database/
```

## Shared Context

Lint rules use a shared context defined in `sample_project/spec/spec_helper.rb` that provides pre-built collections:
Lint rules use a shared context that provides pre-built collections.

For RSpec, it's defined in `sample_project/spec/spec_helper.rb`:

```ruby
let(:project) { Rubyzen::Project.new([sample_src]) }
let(:controllers) { project.files.with_paths('src/controllers/').classes }
let(:models) { project.files.with_paths('src/models/').classes }
let(:services) { project.files.with_paths('src/services/').classes }
```

For Minitest, the equivalent is the `LintTestCase` base class in `sample_project/test/test_helper.rb`, which exposes the same collections as memoized methods (`controllers`, `models`, `services`, …) — subclass it in your lint rule and call them directly.
17 changes: 15 additions & 2 deletions .claude/skills/run-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ description: Run Rubyzen's unit test suite. Use this skill when the user wants t

# Running Rubyzen Unit Tests

Unit tests verify the correctness of Rubyzen's own API — declarations, providers, collections, and matchers.
Unit tests verify the correctness of Rubyzen's own API — declarations, providers, collections, RSpec matchers, and Minitest assertions. Rubyzen has two test suites: RSpec specs in `spec/` and Minitest tests in `test/`.

## Run All Tests

```bash
bundle exec rspec spec/
# Both suites (RSpec then Minitest)
bundle exec rake

# RSpec only
bundle exec rspec spec

# Minitest only
bundle exec rake test
```

## Run a Specific File
Expand Down Expand Up @@ -49,6 +56,12 @@ spec/
├── project_spec.rb # Project class tests
└── cache/
└── parse_cache_spec.rb # Caching behavior tests

test/
├── test_helper.rb # Loads rubyzen/minitest + parse helper
├── assertions/
│ └── zen_assertions_test.rb # assert_zen_empty / _true / _false
└── minitest_adapter_test.rb # Entry-point contract test
```

## Interpreting Failures
Expand Down
57 changes: 55 additions & 2 deletions .claude/skills/write-lint-rule/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: write-lint-rule
description: Write an architectural lint rule for a Ruby project using the Rubyzen API. Use this skill when the user wants to create a new lint rule, enforce an architectural constraint, add a code quality check, or write an RSpec test that validates code structure. Also trigger when the user says things like "controllers shouldn't do X", "models must always Y", "enforce that Z", or "add a rule for".
description: Write an architectural lint rule for a Ruby project using the Rubyzen API. Use this skill when the user wants to create a new lint rule, enforce an architectural constraint, add a code quality check, or write an RSpec or Minitest test that validates code structure. Also trigger when the user says things like "controllers shouldn't do X", "models must always Y", "enforce that Z", or "add a rule for".
---

# Writing a Lint Rule with Rubyzen
Expand Down Expand Up @@ -45,7 +45,7 @@ SCOPE → FILTER → EXTRACT → ASSERT
Lint rules use a shared context in `spec_helper.rb` that provides pre-built collections. Read the existing `spec_helper.rb` in the target project's spec directory. If it doesn't exist, create one:

```ruby
require 'rubyzen'
require 'rubyzen/rspec'

RSpec.configure do |config|
RSpec.shared_context 'project_config' do
Expand Down Expand Up @@ -136,6 +136,59 @@ expect(models).to zen_false { |m| m.name.end_with?('Helper') }
expect(models).to zen_false(baseline: ['OldHelper']) { |m| m.name.end_with?('Helper') }
```

## Minitest variant

Rubyzen includes a Minitest adapter alongside the RSpec matchers. The `SCOPE → FILTER → EXTRACT → ASSERT` model is identical — only the assertion call changes. The assertions mirror the matchers one-to-one, including the same `allowlist:` / `baseline:` support and the same rich failure messages.

**Shared context** — define a `test/test_helper.rb` that requires the Minitest adapter and provides a base class with pre-built collections (the Minitest counterpart of the RSpec shared context):

```ruby
# test/test_helper.rb
require 'rubyzen/minitest'
require 'minitest/autorun'

class LintTestCase < Minitest::Test
def project = @project ||= Rubyzen::Project.new

# Scope collections by directory
def controllers = @controllers ||= project.files.with_paths('src/controllers/').classes
def models = @models ||= project.files.with_paths('src/models/').classes
def services = @services ||= project.files.with_paths('src/services/').classes
# Add more as needed...
end
```

**Assertion equivalents:**

| RSpec matcher | Minitest assertion |
|---|---|
| `expect(c).to zen_empty` | `assert_zen_empty(c)` |
| `expect(c).to zen_empty("msg")` | `assert_zen_empty(c, message: "msg")` |
| `expect(c).to zen_empty(allowlist: [...], baseline: [...])` | `assert_zen_empty(c, allowlist: [...], baseline: [...])` |
| `expect(c).to zen_true { \|x\| ... }` | `assert_zen_true(c) { \|x\| ... }` |
| `expect(c).to zen_false { \|x\| ... }` | `assert_zen_false(c) { \|x\| ... }` |

Note the custom message is a positional arg in RSpec but the `message:` keyword in Minitest.

**Example:**

```ruby
require 'test_helper'

# `controllers`, `models`, etc. are available from the LintTestCase base class
class ControllersTest < LintTestCase
def test_controllers_do_not_use_where
assert_zen_empty(controllers.all_methods.call_sites.with_name('where'))
end

def test_controllers_inherit_from_application_controller
assert_zen_true(controllers) { |c| c.superclass_name == 'ApplicationController' }
end
end
```

The `{ }`-vs-`do...end` precedence caveat does **not** apply here (the block binds to the assertion method call either way), but `{ }` is still recommended for consistency.

## Common API Patterns

### Filtering by file path
Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ What actually happened. Include error messages or test output if applicable.

## Environment

- Ruby version:
- Rubyzen version:
- RSpec version:
- Ruby version:
- Test framework & version (RSpec or Minitest):
3 changes: 2 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
### Checklist

- [ ] Tests are added or updated for the changes
- [ ] `bundle exec rspec spec/` passes locally
- [ ] YARD docs are added or updated for the changes
- [ ] `bundle exec rake` passes locally (runs both the RSpec and Minitest suites)
7 changes: 5 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ jobs:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true

- name: Run unit tests
run: bundle exec rspec spec/
- name: Run RSpec unit tests
run: bundle exec rake spec

- name: Run Minitest unit tests
run: bundle exec rake test
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

## [0.2.0]

### Added

- **Minitest adapter.** Write architectural lint rules with Minitest using
`require 'rubyzen/minitest'`. This provides the `assert_zen_empty`, `assert_zen_true`,
and `assert_zen_false` assertions, which mirror the RSpec matchers.

### Changed

- **`rspec` is no longer a runtime dependency.** Rubyzen no longer depends
on RSpec at runtime; RSpec users should add `gem 'rspec'` (or `rspec-rails`)
to their Gemfile. This allows Minitest users to not depend on RSpec.

### Migration from 0.1.x

- Change `require 'rubyzen'` to `require 'rubyzen/rspec'`.
- Ensure `gem 'rspec'` is in your Gemfile's test group.

## [0.1.0]

- Initial release
Loading