jdaugherty commented on code in PR #15340:
URL: https://github.com/apache/grails-core/pull/15340#discussion_r2738665036


##########
.github/copilot-instructions.md:
##########
@@ -0,0 +1 @@
+../AGENTS.md

Review Comment:
   I don't think this is necessary - copilot will find the closest AGENTS.md 
file in your repo per this: 
https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions
 



##########
.agents/skills/grails-developer/SKILL.md:
##########
@@ -0,0 +1,883 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+---
+name: grails-developer
+description: Comprehensive guide for Grails 7 development, covering web 
applications, REST APIs, GORM, controllers, services, views, plugins, and 
testing with Spock and Geb
+license: Apache-2.0
+compatibility: opencode, claude, grok, gemini, copilot, cursor, windsurf
+metadata:
+  audience: developers
+  frameworks: grails
+  versions: 7
+---
+
+## What I Do
+
+- Provide detailed guidance for building Grails 7 web applications and REST 
APIs.
+- Assist with GORM for data modeling, controllers for request handling, 
services for business logic, and views (GSP, JSON, Markup).
+- Support testing with Spock 2.3 (unit/integration tests) and Geb for browser 
automation.
+- Guide plugin usage and development, security implementation, and deployment 
strategies.
+- Help with configuration, internationalization, async programming, and 
performance optimization.
+
+## When to Use Me
+
+Activate this skill when developing with Grails 7, including:
+
+- Building CRUD applications, RESTful APIs, or full-stack web applications.
+- Working with GORM domain classes, constraints, and relationships.
+- Creating controllers, services, interceptors, or tag libraries.
+- Writing Spock specifications or Geb functional tests.
+- Configuring plugins, security, caching, or database migrations.
+- Deploying Grails applications to containers or cloud platforms.
+
+## Technology Stack
+
+Grails 7 is built on:
+- **Spring Boot**: 3.5.x
+- **Spring Framework**: 6.2.x
+- **Groovy**: 4.0.x
+- **Gradle**: 8.14.x
+- **Spock**: 2.3-groovy-4.0
+- **Jakarta EE**: 10 (migrated from javax.*)
+- **Micronaut**: Optional via `grails-micronaut` plugin
+
+## Project Structure
+
+```
+myapp/
+├── grails-app/
+│   ├── conf/                 # Configuration
+│   │   ├── application.yml   # Main config
+│   │   ├── logback.xml       # Logging config
+│   │   └── spring/           # Spring bean definitions
+│   ├── controllers/          # Request handlers
+│   ├── domain/               # GORM domain classes
+│   ├── i18n/                 # Message bundles
+│   ├── init/                 # Bootstrap classes
+│   ├── services/             # Business logic
+│   ├── taglib/               # Custom GSP tags
+│   ├── utils/                # Utility classes
+│   └── views/                # GSP templates
+├── src/
+│   ├── main/groovy/          # Additional Groovy classes
+│   ├── main/java/            # Java classes
+│   └── test/groovy/          # Test specifications
+├── build.gradle              # Build configuration
+└── gradle.properties         # Project properties
+```
+
+## Domain Classes (GORM)
+
+### Basic Domain Class
+```groovy
+class Book {
+    String title
+    String author
+    Date datePublished
+    Integer pages
+
+    static constraints = {
+        title blank: false, maxSize: 255
+        author blank: false
+        datePublished nullable: true
+        pages min: 1, nullable: true
+    }
+
+    static mapping = {
+        table 'books'
+        title column: 'book_title'
+        datePublished type: 'date'
+    }
+}
+```
+
+### Relationships
+```groovy
+class Author {
+    String name
+
+    static hasMany = [books: Book]
+
+    static mapping = {
+        books cascade: 'all-delete-orphan'
+    }
+}
+
+class Book {
+    String title
+
+    static belongsTo = [author: Author]
+}
+
+// Many-to-Many
+class Book {
+    static hasMany = [categories: Category]
+    static belongsTo = Category
+}
+
+class Category {
+    String name
+    static hasMany = [books: Book]
+}
+```
+
+### GORM Queries
+```groovy
+// Dynamic finders
+Book.findByTitle("Grails Guide")
+Book.findAllByAuthorLike("%Smith%")
+Book.findByTitleAndAuthor("Guide", "Smith")
+Book.countByPagesGreaterThan(100)
+
+// Where queries
+Book.where {
+    title =~ "%Grails%" && pages > 100
+}.list()
+
+// Criteria queries
+Book.withCriteria {
+    like('title', '%Grails%')
+    gt('pages', 100)
+    order('datePublished', 'desc')
+    maxResults(10)
+}
+
+// HQL queries
+Book.executeQuery(
+    "from Book b where b.author.name = :name",
+    [name: 'Smith']
+)
+
+// Detached criteria (reusable)
+def criteria = Book.where {
+    pages > 100
+}
+criteria.list()
+criteria.count()
+```
+
+### Transactions
+```groovy
+// In services (default transactional)
+@Transactional
+class BookService {
+    def saveBook(Book book) {
+        book.save(failOnError: true)
+    }
+}
+
+// Manual transaction control
+Book.withTransaction { status ->
+    def book = new Book(title: "Test")
+    book.save()
+    if (someCondition) {
+        status.setRollbackOnly()
+    }
+}
+```
+
+## Controllers
+
+### Basic Controller
+```groovy
+class BookController {
+
+    BookService bookService
+
+    static allowedMethods = [save: 'POST', update: 'PUT', delete: 'DELETE']
+
+    def index() {
+        respond Book.list(), model: [bookCount: Book.count()]
+    }
+
+    def show(Long id) {
+        respond Book.get(id)
+    }
+
+    def create() {
+        respond new Book(params)
+    }
+
+    def save(Book book) {
+        if (book.hasErrors()) {
+            respond book.errors, view: 'create'
+            return
+        }
+        bookService.save(book)
+        redirect action: 'show', id: book.id
+    }
+
+    def edit(Long id) {
+        respond Book.get(id)
+    }
+
+    def update(Book book) {
+        if (book.hasErrors()) {
+            respond book.errors, view: 'edit'
+            return
+        }
+        bookService.save(book)
+        redirect action: 'show', id: book.id
+    }
+
+    def delete(Long id) {
+        bookService.delete(id)
+        redirect action: 'index'
+    }
+}
+```
+
+### REST Controller
+```groovy
+import grails.rest.RestfulController
+
+class BookController extends RestfulController<Book> {
+
+    static responseFormats = ['json', 'xml']
+
+    BookController() {
+        super(Book)
+    }
+
+    // Override to customize
+    @Override
+    protected Book queryForResource(Serializable id) {
+        Book.where { id == id && active == true }.find()
+    }
+}
+```
+
+### Command Objects
+```groovy
+class BookCommand implements Validateable {
+    String title
+    String author
+    Integer pages
+
+    static constraints = {
+        title blank: false
+        author blank: false
+        pages min: 1, nullable: true
+    }
+}
+
+class BookController {
+    def save(BookCommand cmd) {
+        if (cmd.hasErrors()) {
+            respond cmd.errors
+            return
+        }
+        // Process valid command
+    }
+}
+```
+
+### Content Negotiation
+```groovy
+class BookController {
+    def show(Long id) {
+        def book = Book.get(id)
+        respond book  // Auto-negotiates based on Accept header
+    }
+}
+
+// Or explicit
+def show(Long id) {
+    def book = Book.get(id)
+    withFormat {
+        html { render view: 'show', model: [book: book] }
+        json { render book as JSON }
+        xml { render book as XML }
+    }
+}
+```
+
+## Services
+
+### Basic Service
+```groovy
+import grails.gorm.transactions.Transactional
+
+@Transactional
+class BookService {
+
+    def findByTitle(String title) {
+        Book.findByTitle(title)
+    }
+
+    def save(Book book) {
+        book.save(failOnError: true)
+    }
+
+    @Transactional(readOnly = true)
+    def list(Map params) {
+        Book.list(params)
+    }
+
+    def delete(Long id) {
+        Book.get(id)?.delete()
+    }
+}
+```
+
+### Service with Events
+```groovy
+import grails.events.annotation.Publisher
+import grails.events.annotation.Subscriber
+
+class BookService {
+
+    @Publisher
+    def save(Book book) {
+        book.save()
+        // Automatically publishes 'book.saved' event
+    }
+}
+
+class NotificationService {
+
+    @Subscriber('book.saved')
+    def onBookSaved(Book book) {
+        // Handle event
+        log.info "Book saved: ${book.title}"
+    }
+}
+```
+
+### Async Service
+```groovy
+import grails.async.Promise
+import static grails.async.Promises.*
+
+class BookService {
+
+    Promise<Book> findAsync(Long id) {
+        task {
+            Book.get(id)
+        }
+    }
+
+    def processBooks() {
+        def p1 = task { Book.findAllByActive(true) }
+        def p2 = task { Author.list() }
+
+        waitAll(p1, p2)
+        // Both complete
+    }
+}
+```
+
+## Views
+
+### GSP (Groovy Server Pages)
+```html
+<!-- grails-app/views/book/list.gsp -->
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Books</title>
+    <asset:stylesheet src="application.css"/>
+</head>
+<body>
+    <h1>Books (${bookCount})</h1>
+
+    <g:each in="${bookList}" var="book">
+        <div class="book">
+            <h2>${book.title}</h2>
+            <p>By ${book.author}</p>
+            <g:link action="show" id="${book.id}">View</g:link>
+        </div>
+    </g:each>
+
+    <g:if test="${bookList.empty}">
+        <p>No books found.</p>
+    </g:if>
+
+    <g:paginate total="${bookCount}"/>
+
+    <asset:javascript src="application.js"/>
+</body>
+</html>
+```
+
+### Common GSP Tags
+```html
+<!-- Links -->
+<g:link controller="book" action="show" id="${book.id}">View</g:link>
+<g:createLink action="list"/>
+
+<!-- Forms -->
+<g:form action="save">
+    <g:textField name="title" value="${book?.title}"/>
+    <g:textArea name="description" value="${book?.description}"/>
+    <g:select name="category" from="${categories}" optionKey="id" 
optionValue="name"/>
+    <g:checkBox name="active" value="${book?.active}"/>
+    <g:submitButton name="submit" value="Save"/>
+</g:form>
+
+<!-- Conditionals -->
+<g:if test="${condition}">...</g:if>
+<g:elseif test="${other}">...</g:elseif>
+<g:else>...</g:else>
+
+<!-- Iteration -->
+<g:each in="${items}" var="item" status="i">
+    ${i}: ${item.name}
+</g:each>
+
+<!-- Rendering -->
+<g:render template="bookRow" model="[book: book]"/>
+<g:render template="bookRow" collection="${books}" var="book"/>
+
+<!-- Security (with Spring Security plugin) -->
+<sec:ifLoggedIn>Welcome, ${sec.username()}</sec:ifLoggedIn>
+<sec:ifNotLoggedIn><g:link 
controller="login">Login</g:link></sec:ifNotLoggedIn>
+```
+
+### JSON Views
+```groovy
+// grails-app/views/book/show.gson
+import myapp.Book
+
+model {
+    Book book
+}
+
+json {
+    id book.id
+    title book.title
+    author book.author
+    datePublished book.datePublished?.format('yyyy-MM-dd')
+    _links {
+        self {
+            href "/books/${book.id}"
+        }
+    }
+}
+```
+
+### Layouts
+```html
+<!-- grails-app/views/layouts/main.gsp -->
+<!DOCTYPE html>
+<html>
+<head>
+    <title><g:layoutTitle default="My App"/></title>
+    <asset:stylesheet src="application.css"/>
+    <g:layoutHead/>
+</head>
+<body>
+    <nav><!-- Navigation --></nav>
+    <main>
+        <g:layoutBody/>
+    </main>
+    <footer><!-- Footer --></footer>
+    <asset:javascript src="application.js"/>
+</body>
+</html>
+
+<!-- Use in views -->
+<html>
+<head>
+    <meta name="layout" content="main"/>
+    <title>Books</title>
+</head>
+<body>
+    <!-- Page content -->
+</body>
+</html>
+```
+
+## Interceptors
+
+```groovy
+class AuthInterceptor {
+
+    AuthInterceptor() {
+        matchAll()
+            .excludes(controller: 'login')
+            .excludes(controller: 'error')
+    }
+
+    boolean before() {
+        if (!session.user) {
+            redirect(controller: 'login')
+            return false
+        }
+        true
+    }
+
+    boolean after() {
+        // After action executes
+        true
+    }
+
+    void afterView() {
+        // After view renders
+    }
+}
+```
+
+## Tag Libraries
+
+```groovy
+class BookTagLib {
+
+    static namespace = 'book'
+    static defaultEncodeAs = [taglib: 'html']
+
+    def formatTitle = { attrs, body ->
+        def title = attrs.title ?: body()
+        out << "<span class='book-title'>${title}</span>"
+    }
+
+    def ifHasBooks = { attrs, body ->
+        if (Book.count() > 0) {
+            out << body()
+        }
+    }
+}
+
+// Usage in GSP
+<book:formatTitle title="${book.title}"/>
+<book:ifHasBooks>
+    <p>We have books!</p>
+</book:ifHasBooks>
+```
+
+## Configuration
+
+### application.yml
+```yaml
+grails:
+    profile: web
+    codegen:
+        defaultPackage: myapp
+    gorm:
+        reactor:
+            events: false
+    mime:
+        types:
+            json: ['application/json', 'text/json']
+            xml: ['text/xml', 'application/xml']
+
+environments:
+    development:
+        dataSource:
+            dbCreate: create-drop
+            url: jdbc:h2:mem:devDb
+    test:
+        dataSource:
+            dbCreate: update
+            url: jdbc:h2:mem:testDb
+    production:
+        dataSource:
+            dbCreate: none
+            url: jdbc:postgresql://localhost/myapp
+            driverClassName: org.postgresql.Driver
+            dialect: org.hibernate.dialect.PostgreSQLDialect
+            properties:
+                jmxEnabled: true
+                initialSize: 5
+                maxActive: 50
+
+server:
+    port: 8080
+    servlet:
+        context-path: /myapp
+```
+
+### URL Mappings
+```groovy
+// grails-app/controllers/myapp/UrlMappings.groovy
+class UrlMappings {
+
+    static mappings = {
+        // Default
+        "/$controller/$action?/$id?(.$format)?" {
+            constraints {
+                // apply constraints here
+            }
+        }
+
+        // REST resources
+        "/api/books"(resources: 'book')
+
+        // Custom mappings
+        "/books/$id"(controller: 'book', action: 'show')
+        "/search"(controller: 'book', action: 'search')
+
+        // Named URL mappings
+        name bookShow: "/books/$id" {
+            controller = 'book'
+            action = 'show'
+        }
+
+        // Error pages
+        "500"(view: '/error')
+        "404"(view: '/notFound')
+    }
+}
+```
+
+## Testing with Spock
+
+### Unit Test - Controller
+```groovy
+import grails.testing.web.controllers.ControllerUnitTest
+import spock.lang.Specification
+
+class BookControllerSpec extends Specification
+        implements ControllerUnitTest<BookController> {
+
+    def setup() {
+        controller.bookService = Mock(BookService)
+    }
+
+    void "index returns list of books"() {
+        given:
+        def books = [new Book(title: "Test")]
+        controller.bookService.list(_) >> books
+
+        when:
+        controller.index()
+
+        then:
+        model.bookList == books
+        view == '/book/index'
+    }
+
+    void "show returns 404 for missing book"() {
+        when:
+        controller.show(999)
+
+        then:
+        response.status == 404
+    }
+}
+```
+
+### Unit Test - Service
+```groovy
+import grails.testing.services.ServiceUnitTest
+import spock.lang.Specification
+
+class BookServiceSpec extends Specification
+        implements ServiceUnitTest<BookService> {
+
+    void "save persists book"() {
+        given:
+        def book = new Book(title: "Test", author: "Author")
+
+        when:
+        def result = service.save(book)
+
+        then:
+        result.id != null
+        Book.count() == 1
+    }
+}
+```
+
+### Unit Test - Domain
+```groovy
+import grails.testing.gorm.DomainUnitTest
+import spock.lang.Specification
+
+class BookSpec extends Specification
+        implements DomainUnitTest<Book> {
+
+    void "title cannot be blank"() {
+        when:
+        domain.title = ""
+        domain.author = "Test"
+
+        then:
+        !domain.validate(['title'])
+        domain.errors['title'].code == 'blank'
+    }
+
+    void "valid book passes validation"() {
+        when:
+        domain.title = "Valid Title"
+        domain.author = "Author"
+
+        then:
+        domain.validate()
+    }
+}
+```
+
+### Integration Test
+```groovy
+import grails.testing.mixin.integration.Integration
+import grails.gorm.transactions.Rollback
+import spock.lang.Specification
+
+@Integration
+@Rollback
+class BookServiceIntegrationSpec extends Specification {
+
+    BookService bookService
+
+    void "save and retrieve book"() {
+        given:
+        def book = new Book(title: "Integration Test", author: "Tester")
+
+        when:
+        bookService.save(book)
+        def found = Book.findByTitle("Integration Test")
+
+        then:
+        found != null
+        found.author == "Tester"
+    }
+}
+```
+
+### Functional Test with Geb
+```groovy
+import geb.spock.GebSpec
+import grails.testing.mixin.integration.Integration
+
+@Integration
+class BookFunctionalSpec extends GebSpec {
+
+    void "can view book list"() {
+        when:
+        go '/book/index'
+
+        then:
+        title == 'Book List'
+        $('h1').text() == 'Books'
+    }
+
+    void "can create new book"() {
+        when:
+        go '/book/create'
+        $('form').title = 'New Book'
+        $('form').author = 'Test Author'
+        $('input[type=submit]').click()
+
+        then:
+        $('h1').text().contains('New Book')
+    }
+}
+```
+
+## Plugins
+
+### Common Plugins
+```groovy
+// build.gradle
+
+// Spring Security
+implementation 'org.grails.plugins:spring-security-core:6.0.0'
+
+// Database Migration (Liquibase)
+implementation 'org.grails.plugins:database-migration:4.2.0'
+
+// Caching
+implementation 'org.grails.plugins:cache:6.0.0'
+
+// Async support
+implementation 'org.grails.plugins:async:5.0.0'
+
+// Fields plugin for form rendering
+implementation 'org.grails.plugins:fields:5.0.0'
+
+// Asset Pipeline
+runtimeOnly 'org.grails.plugins:asset-pipeline:5.0.0'
+```
+
+### Spring Security Configuration
+```groovy
+// grails-app/conf/application.groovy
+grails.plugin.springsecurity.userLookup.userDomainClassName = 'myapp.User'
+grails.plugin.springsecurity.userLookup.authorityJoinClassName = 
'myapp.UserRole'
+grails.plugin.springsecurity.authority.className = 'myapp.Role'
+
+grails.plugin.springsecurity.controllerAnnotations.staticRules = [
+    [pattern: '/',               access: ['permitAll']],
+    [pattern: '/error',          access: ['permitAll']],
+    [pattern: '/index',          access: ['permitAll']],
+    [pattern: '/admin/**',       access: ['ROLE_ADMIN']],
+    [pattern: '/api/**',         access: ['isAuthenticated()']],
+]
+```
+
+## Build Commands
+
+```bash
+# Run application
+./gradlew bootRun
+
+# Run tests
+./gradlew test                                    # All tests

Review Comment:
   why not make this `./gradlew check` instead?  I know that unit & integration 
tests both extend the test type, but check is meant to run everything I thought.



##########
.cursorrules:
##########
@@ -0,0 +1 @@
+AGENTS.md

Review Comment:
   Do you think it's beneficial to add these other editor links? 



##########
AGENTS.md:
##########
@@ -0,0 +1,227 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Agent Guide for grails-core
+
+> **IMPORTANT**: This is the Grails Framework source repository (60+ modules), 
NOT a Grails application.
+> For building Grails apps, see `.agents/skills/grails-developer/SKILL.md`.
+
+## Quick Reference
+
+```bash
+# Build (no tests)
+./gradlew build -PskipTests
+
+# Build single module
+./gradlew :grails-core:build
+
+# Run tests
+./gradlew :<module>:test
+./gradlew :<module>:test --tests "com.example.SomeSpec"
+
+# Style check
+./gradlew codeStyle
+
+# Out of memory? Set:
+export GRADLE_OPTS="-Xms2G -Xmx5G"
+```
+
+## Critical Rules
+
+1. **Use `jakarta.*` NOT `javax.*`** - All packages migrated to Jakarta EE 10
+2. **Use `@GrailsCompileStatic`** - Not plain `@CompileStatic` in Grails 
classes
+3. **Use `GrailsWebRequest.lookup()`** - For thread-safe request context in 
tests
+4. **No wildcard imports** - Use explicit imports
+5. **4 spaces, no tabs** - See `.editorconfig`
+6. **Apache license header** - Required on all new source files
+
+## Available Skills
+
+> **AI AGENTS - MANDATORY**: Before writing or modifying any code, you 
**MUST** read the relevant skill file(s) below. Do not write Groovy/Grails code 
without first loading these instructions:
+> - Writing Grails code → Read `.agents/skills/grails-developer/SKILL.md`
+> - Writing Groovy code → Read `.agents/skills/groovy-developer/SKILL.md`
+> - Writing Java code → Read `.agents/skills/java-developer/SKILL.md`
+>
+> Use your file reading capability to load the skill content before proceeding 
with any code changes.
+
+| Skill | Path | Use For |
+|-------|------|---------|
+| **grails-developer** | `.agents/skills/grails-developer/SKILL.md` | Grails 7 
apps, GORM, controllers, views |
+| **groovy-developer** | `.agents/skills/groovy-developer/SKILL.md` | Groovy 4 
syntax, closures, DSLs, Spock |
+| **java-developer** | `.agents/skills/java-developer/SKILL.md` | Java 17 
features, Groovy interop |
+
+## Technology Stack
+
+| Component | Version |
+|-----------|---------|
+| JDK | 17+ (baseline 17) |
+| Groovy | 4.0.x |
+| Spring Boot | 3.5.x |
+| Spring Framework | 6.2.x |
+| Spock | 2.3-groovy-4.0 |
+| Gradle | 8.14.x |
+| Jakarta EE | 10 |
+
+## Key Modules
+
+**Core**: `grails-core`, `grails-bootstrap`, `grails-spring`, `grails-common`
+
+**Web**: `grails-web-core`, `grails-web-mvc`, `grails-controllers`, 
`grails-url-mappings`, `grails-interceptors`
+
+**GORM**: `grails-datastore-core`, `grails-datamapping-core`, 
`grails-domain-class`, `grails-validation`, `grails-databinding`
+
+**Views**: `grails-views-core`, `grails-views-gson`, `grails-views-markup`
+
+**Testing**: `grails-testing-support-core`, `grails-testing-support-web`, 
`grails-geb`
+
+**Other**: `grails-bom` (dependency management), `grails-doc`, 
`grails-shell-cli`, `grails-forge`
+
+## Artefact Types
+
+| Type | Pattern | Handler |
+|------|---------|---------|
+| Domain | `**/domain/**/*.groovy` | `DomainClassArtefactHandler` |
+| Controller | `**/*Controller.groovy` | `ControllerArtefactHandler` |
+| Service | `**/*Service.groovy` | `ServiceArtefactHandler` |
+| TagLib | `**/*TagLib.groovy` | `TagLibArtefactHandler` |
+| Interceptor | `**/*Interceptor.groovy` | `InterceptorArtefactHandler` |
+
+## Code Patterns
+
+### Spock Test Structure
+```groovy
+class MyServiceSpec extends Specification implements 
ServiceUnitTest<MyService> {
+    def "feature description"() {
+        given: "preconditions"
+        def input = "test"
+
+        when: "action"
+        def result = service.process(input)
+
+        then: "assertions"
+        result != null
+        result.status == "OK"
+    }
+}
+```
+
+### Mocking
+```groovy
+def repo = Mock(BookRepository)
+1 * repo.save(_) >> savedBook  // expect 1 call, return savedBook
+```
+
+### Data-Driven Tests
+```groovy
+@Unroll
+def "#a + #b == #c"() {
+    expect: a + b == c
+    where:
+    a | b || c
+    1 | 2 || 3
+    4 | 5 || 9
+}
+```
+
+### Framework Access
+```groovy
+// Thread-safe request context
+GrailsWebRequest webRequest = GrailsWebRequest.lookup()
+
+// Artefact registry
+grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE)
+```
+
+## Groovy Style
+
+```groovy
+// DO: Safe navigation
+book?.author?.name
+
+// DO: Elvis operator
+name ?: 'Unknown'
+
+// DO: GStrings
+"Hello ${user.name}"
+
+// DO: Spread operator
+books*.title
+
+// DO: Static compilation
+@GrailsCompileStatic
+class MyService { }
+
+// DON'T: Wildcard imports
+// import java.util.*  ❌
+
+// DON'T: javax packages
+// import javax.servlet.*  ❌ → use jakarta.servlet.*
+```
+
+## Test Isolation
+
+> **WARNING**: Tests run in parallel (`maxParallelForks > 1`). Static state 
causes flaky tests.
+
+- Use `GrailsWebRequest.lookup()` for thread-local context
+- Clear artefacts: `grailsApplication.artefactInfo.clear()`
+- Use `@ResourceLock` for shared resources
+
+## Build Commands
+
+| Task | Command |
+|------|---------|
+| Build (no tests) | `./gradlew build -PskipTests` |
+| Build module | `./gradlew :grails-core:build` |
+| Test module | `./gradlew :grails-core:test` |
+| Single test | `./gradlew :module:test --tests "pkg.MySpec"` |
+| Single feature | `./gradlew :module:test --tests "pkg.MySpec.feature name"` |
+| Force rerun | `./gradlew :module:test --rerun-tasks` |
+| Style check | `./gradlew codeStyle` |
+| Build docs | `./gradlew :grails-doc:publishGuide -x aggregateGroovydoc` |
+| Debug | `./gradlew bootRun --debug-jvm` |
+
+## Branch Naming (Auto-Labels PRs)
+
+| Prefix | Label |
+|--------|-------|
+| `fix/` | bug |
+| `feat/`, `feature/` | feature |
+| `docs/` | documentation |
+| `chore/`, `refactor/`, `test/`, `ci/`, `perf/`, `build/` | maintenance |
+| `deps/` | deps |
+
+## Common Issues
+
+| Problem | Solution |
+|---------|----------|
+| Out of memory | `export GRADLE_OPTS="-Xms2G -Xmx5G"` |
+| Container missing | Use `-PskipTests` or install Docker/Podman |
+| Flaky tests | Check static state pollution, use `@ResourceLock` |
+| Cache issues | `./gradlew --rerun-tasks` |
+| Deprecation details | `./gradlew <task> --warning-mode all` |
+
+## Security
+
+Report vulnerabilities to: `[email protected]` (NOT public issues)
+

Review Comment:
   https://github.com/agentsmd/agents.md suggests the following additional 
sections: 
   * Testing instructions 
   * Dev environment tips - since we have a multiple project build should we 
call out the multiproject structure?  i.e. build-logic vs grails-gradle vs 
grails-core vs grails-forge? 
   * PR instructions



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to