↑ ↓ to move · Enter to open · Esc to closeTap a result

Architecture
skill · class design · opus

test-architect

Designs the tests: what is tested at which level, the data, the doubles and the coverage gaps.

skill · designopuswrites 40-testes.mdonly inside docs/use-cases/UC-*/**
§ the file it writes

40-testes.md, block by block

A real partial: UC-001-create-customer, in bank-app, excerpted. Hover or click a block to see what the skill fixes there. The sections of the file are also chips: click one to jump to it.

bank-app/docs/use-cases/UC-001-create-customer/40-testes.md

UC-001 · Tests

Partial of docs/use-cases/UC-001-create-customer/. Owner: test-architect. Inherits the invariants from 10-dominio.md, the queries from 20-persistencia.md, and the contract cases from 30-rest.md — doesn't reinvent them. Contains no code. Reference shapes in .claude/skills/test-architect/templates/. Rules applied, never reproduced: @.claude/rules/testing.md.
FieldValue
Partials read00, 10, 20, 30 (no 25, 28, 32, 35 — none applies)
Engine in integration testsPostgreSQL 16 (postgres:16-alpine), ephemeral container via the existing TestcontainersConfiguration
Invariants from 10-dominio.md11
Invariants with a named test11 (two of them set by the use case, covered at aggregate level only)

Test paths mirror production under src/test/java/dev/nerviz/bankapp/.

## 1 · Distribution by level

BehaviorLevelWhy here
SecurityNumber format, trim, maskingDomain unitValue-object rule, no context
Customer name, date and age rules, leap day, rehydrate skipping the date rulesDomain unitAggregate invariants, fixed clock
Duplicate security number aborts before saving; format checked before the lookupApplication unitOrchestration, CustomerRepository replaced by a double
Command and request DTO never print the security number or birth dateUnitMasking — @.claude/rules/logging.md § Masking candidates
Idempotency-Key header presence and formatUnitInterceptor component, no context
HTTP contract (201, 400, 409, 422, 500)Web sliceCases defined in 30-rest.md § 5 — here only level and data. Use case is a double, so Test, not IT
Mapping, char(11), re-read of the aggregate, unique constraint translationIntegrationOnly fails against the real engine
Idempotency store: claim, replay, reuse, in-progress, reclaim, releaseIntegrationTwo-transaction shape and PK collision only exist in the real database
Idempotent replay end to end (aspect + store + use case) — one customer for two identical callsIntegrationThe aspect, the store and the transaction meet only in the full context
Dependency direction and namesArchitectureOnce per project — ArchitectureTest not installed yet; test-architect setup mode at the /new-feature pre-flight

Contract replay cases of 30-rest.md § 5 ("Idempotent replay", "Reused key") run in CreateCustomerIdempotencyIT, not in the web slice: the slice doesn't load the aspect or a database, and a replay asserted against a double proves nothing.

Change outside this case's classes: BankAppApplicationTests runs @SpringBootTest without @ActiveProfiles("test") — it gets the annotation (@.claude/rules/testing.md § Slices and context). Not an approved case's file: the bootstrap's own.

## 2 · Cases per test class

Fixed clock in every domain and application test: 2026-01-15T10:00:00Z, UTC — today is 2026-01-15.

ClassLevelMethods
domain/model/SecurityNumberTestDomainacceptsElevenDigits · trimsSurroundingWhitespace (" 12345678901 ") · rejectsMissingValue → SECURITY_NUMBER_REQUIRED · rejectsMalformedValue (parameterized: "1234567890", "123456789012", "123.456.789-01", "1234567890a", "", " ") → SECURITY_NUMBER_INVALID · masksAllButLastTwoDigits ("*********01") · toStringIsMasked
domain/model/CustomerTestDomainregistersWithTrimmedNameAndClockInstant · rejectsMissingId → CUSTOMER_ID_REQUIRED · rejectsBlankName (parameterized: null, "", " ") → CUSTOMER_NAME_REQUIRED · rejectsNameOutsideLength (parameterized: "A", " A ", 121 × "a") → CUSTOMER_NAME_LENGTH · acceptsNameAtBounds (parameterized: 2 and 120 characters) · rejectsMissingSecurityNumber → SECURITY_NUMBER_REQUIRED · rejectsMissingBirthDate → BIRTH_DATE_REQUIRED · rejectsFutureBirthDate (2026-01-16) → BIRTH_DATE_IN_FUTURE · rejectsAgeOver130 (1895-01-15, 131st birthday today) → BIRTH_DATE_TOO_OLD · acceptsAge130 (1895-01-16) · rejectsCustomerNotStrictlyOver18 (parameterized: 2008-01-15 — 18th birthday today, 2008-01-16, 2010-06-01) → CUSTOMER_UNDERAGE · accepts18thBirthdayYesterday (2008-01-14) · appliesFebruary28AnniversaryForLeapDayBirth (parameterized over the clock: born 2008-02-29; clock 2026-02-28 → CUSTOMER_UNDERAGE, clock 2026-03-01 → accepted) · rehydrateDoesNotReapplyDateRules (born 2015-03-10) · rejectsMissingRegisteredAtOnRehydrate → REGISTERED_AT_REQUIRED
application/usecase/customer/CreateCustomerUseCaseTestApplicationcreatesCustomerAndReturnsItsId (asserts the saved aggregate's fields and that the returned id is the saved one) · rejectsAlreadyRegisteredSecurityNumber → SECURITY_NUMBER_ALREADY_REGISTERED, save never called · rejectsMalformedSecurityNumberBeforeLookup → SECURITY_NUMBER_INVALID, existsBySecurityNumber never called · rejectsUnderageWithoutSaving → CUSTOMER_UNDERAGE
application/usecase/customer/CreateCustomerCommandTestUnittoStringMasksSecurityNumberAndBirthDate — raw 12345678901 and 1990-05-17 absent
infrastructure/rest/dto/CreateCustomerRequestTestUnitmaskedRepresentationHidesSecurityNumberAndBirthDate — renders the DTO the way GlobalHttpMethodLogAspect does (LogMask), asserts both raw values absent and name present
infrastructure/rest/idempotent/IdempotencyKeyInterceptorTestUnitrejectsMissingKey · rejectsMalformedKey (parameterized: "abc", "", "123") · acceptsUuidKey · ignoresHandlerWithoutIdempotent — shape templates/IdempotencyKeyInterceptorTest.java.example
infrastructure/rest/CustomerControllerTestWeb sliceOne method per row of 30-rest.md § 5, except "Idempotent replay" and "Reused key" (below): createsCustomerReturningLocationAndIdOnly · rejectsUnderage · rejectsDuplicateSecurityNumber · rejectsInvalidSecurityNumber (parameterized: "1234567890", "123.456.789-01") · rejectsBlankName · rejectsNameOutsideLength (parameterized: 1 and 121 characters) · rejectsFutureBirthDate · rejectsAgeOver130 · rejectsMissingField (parameterized: name, securityNumber, birthDate — violations[0].field equals it, errorCode absent) · rejectsUnparseableBirthDate · rejectsMissingIdempotencyKey · hidesInternalFailureBehindTraceId · neverEchoesPersonalDataInErrors (parameterized over the error cases above) — shape templates/ControllerTest.java.example
infrastructure/persistence/customer/CustomerRepositoryJpaAdapterITIntegrationpersistsAndRereadsTheSameAggregate (reads back through CustomerJpaRepository + mapper: name, 11-digit number, birth date, registeredAt) · reportsWhetherSecurityNumberExists · rejectsDuplicateSecurityNumber → SECURITY_NUMBER_ALREADY_REGISTERED asserted on the exception, not only the type — proves the flush inside the adapter — shape templates/PersistenceIT.java.example
infrastructure/persistence/idempotency/IdempotencyKeyStoreITIntegrationclaimsNewKey · replaysCompletedResponse · rejectsSameKeyWithDifferentBody → IDEMPOTENCY_KEY_REUSED · rejectsKeyStillInProgress → IDEMPOTENCY_KEY_IN_PROGRESS · reclaimsKeyAfterLeaseExpires · releaseFreesKeyForRetry
infrastructure/rest/CreateCustomerIdempotencyITIntegration (@SpringBootTest, MockMvc, @ActiveProfiles("test"))replaysOriginalResponseForSameKey — two identical POSTs, both 201 with identical Location and body, exactly one row in customers · rejectsSameKeyWithDifferentBody → 409, IDEMPOTENCY_KEY_REUSED · releasesKeyWhenCreationFails — an underage POST leaves no idempotency_keys row

Every exception assertion checks the type and errorCode().

## 3 · Data and doubles

Factory — CustomerFixtures (src/test/java/dev/nerviz/bankapp/CustomerFixtures.java, shape templates/TestFixtures.java.example), returns the valid case; each test changes only the field under test.

ElementIn the test
CustomerFixtures.FIXED_CLOCKClock.fixed(2026-01-15T10:00:00Z, UTC)
CustomerFixtures.customer()Customer.register with name "Maria Silva", security number "12345678901", birth date 1990-05-17, fixed clock
CustomerFixtures.command()CreateCustomerCommand("Maria Silva", "12345678901", 1990-05-17)
CustomerFixtures.requestJson()the same values as the controller's JSON body
CustomerRepositoryMockito double in CreateCustomerUseCaseTest: existsBySecurityNumber → false (happy) / true (duplicate); save returns its argument
CreateCustomerUseCaseMockito double (@MockitoBean) in CustomerControllerTest, throwing the domain exception per case
Clock in Spring testsa @TestConfiguration Clock bean fixed at the same instant, overriding ClockConfig — the date cases stay deterministic
Idempotency-Keya literal UUID per test, e.g. 0190f3b2-6c1e-7a40-9b8e-2f1d4c5a6b7c
Databasethe existing TestcontainersConfiguration (postgres:16-alpine); Flyway runs V1, V2 as in production; tables truncated between tests

No double for Customer, SecurityNumber or CustomerId: the real constructors validate.

## 4 · Coverage and gaps

Invariant from 10-dominio.mdTest that guarantees it
1 id presentCustomerTest.rejectsMissingId
2 name not blankCustomerTest.rejectsBlankName
3 name 2–120, trimmedCustomerTest.rejectsNameOutsideLength · acceptsNameAtBounds · registersWithTrimmedNameAndClockInstant
4 security number presentSecurityNumberTest.rejectsMissingValue · CustomerTest.rejectsMissingSecurityNumber
5 security number 11 digitsSecurityNumberTest.rejectsMalformedValue
6 birth date presentCustomerTest.rejectsMissingBirthDate
7 not in the futureCustomerTest.rejectsFutureBirthDate
8 age at most 130CustomerTest.rejectsAgeOver130 · acceptsAge130
9 strictly over 18CustomerTest.rejectsCustomerNotStrictlyOver18 · accepts18thBirthdayYesterday · appliesFebruary28AnniversaryForLeapDayBirth
10 registeredAt presentCustomerTest.rejectsMissingRegisteredAtOnRehydrate
11 security number uniqueCreateCustomerUseCaseTest.rejectsAlreadyRegisteredSecurityNumber + CustomerRepositoryJpaAdapterIT.rejectsDuplicateSecurityNumber

No open gap.

Deliberately left uncovered: CustomerResponse's accessor and ClockConfig (a configuration class, excluded from the gate by **/*Config.class). CustomerMapper is covered through CustomerControllerTest, not by its own test.

Coverage gate (80% lines, 70% branches) is not wired yet — it comes with ArchUnit through test-architect setup mode, offered at the /new-feature pre-flight.

## 5 · Test dependencies

none — JUnit, AssertJ, Mockito and MockMvc come with the spring-boot-starter-*-test starters already in pom.xml; Testcontainers (spring-boot-testcontainers, testcontainers-junit-jupiter, testcontainers-postgresql) is declared. No test waits on an asynchronous effect, so no Awaitility.

## Impact on approved use cases

none — UC-001 is the first case.

Open the full file on GitHub →
§ behavior

What it reads, writes and refuses

The skill's own contract, from SKILL.md, in the original wording.

Class

design — the territory is skill_classes.design in @.claude/schemas/extensions.json and ArchHook.java guard enforces it. That covers both modes: design mode writes inside the use case folder, and setup mode writes nothing itself — every file ArchUnit needs is written by the archunit-installer agent, which the guard bypasses as an executor.

Unfiltered Bash

setup mode verifies with ./mvnw goals that depend on the build shape, plus curl for the Testcontainers image tag and java …ArchHook.java — a fixed list trails the build. Writes stay under guard/guard bash, and a force push is blocked by guard bash (guard.force_push).

Reads

docs/use-cases/UC-NNN-<slug>/00-use-case.md and 10-domain.md (mandatory in design mode — stops without the second), 20-persistence.md, 30-rest.md, 31-graphql.md, 32-security.md, 28-http-client.md, 29-ai.md, 23-search.md, 35-jobs.md and 22-cache.md when they exist, @.claude/rules/search.md § Tests when 23-search.md does, @.claude/rules/ai.md § Tests when 29-ai.md does, @.claude/rules/scheduling.md when 35-jobs.md does, @.claude/rules/caching.md § Tests when 22-cache.md does, @.claude/rules/authorization.md § Tests when 32-security.md does, @.claude/rules/http-client.md § Tests when 28-http-client.md does, @.claude/rules/testing.md, @.claude/rules/error-handling.md, @.claude/rules/naming.md, @.claude/rules/code-quality.md, @.claude/rules/architecture-ddd.md, @.claude/rules/logging.md (masking test requirement for fields 30-rest.md marked sensitive), and the active blueprint's packages.map.

Writes

docs/use-cases/UC-NNN-<slug>/40-tests.md in design mode, directly. Setup mode writes nothing itself — it delegates to archunit-installer, which writes <main-module>/src/test/java//ArchitectureTest.java, the ArchUnit entries in the POM, the jacoco-maven-plugin's check execution, and — the one exception, a single line, not a file — the pinned image tag in the Initializr-generated TestcontainersConfiguration.java. Nothing else in src/test/. Contract of that agent's own reads and writes: @.claude/agents/archunit-installer.md.

Does not edit `docker-compose.yml`

, neither directly nor through the agent, and does not invoke docker-architect either. A tag mismatch against the compose-side service comes back in the agent's summary, and this skill reports it with the /docker-architect command that resolves it — single owner of that file, see its Contract.

Owns the coverage gate

, since @.claude/decisions/0011-bootstrap-without-business-code.md. project-bootstrap remains the owner of the POM and writes JaCoCo instrumenting and reporting; the check execution, which is what turns a report into a gate, comes in here — alongside ArchUnit, because both depend on code existing for them to apply to. Ownership is this skill's; the writing happens in the delegated agent's isolated context.

Doesn't write the use case's tests

The files in src/test/ come from the executor agent. Doesn't edit the other partials and doesn't touch .claude/rules/.

Doesn't duplicate `rest-api-architect`

: the HTTP contract's cases belong to 30-rest.md. This partial cites them and adds level, data, and gaps.

Source: SKILL.md § Contract, at nerviz e4cae27.

§ guard and hook

What the guard does with it

design is the skill's class. ArchHook opens a phase when the skill is called and checks every write against the class's territory. The text on the right is the hook's own.

write_allow of class design

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**: migrations and classes are the executor's
  • docker-compose.yml: another class's file
  • a use case already approved: its specs are frozen

A refusal exits with code 2 and the message goes back to the model.

PreToolUse  Skill(test-architect)
  audit call                       exit 0
  guard call                       exit 0
# a skill of another class replaces the phase
# write_allow: docs/use-cases/UC-*/**, docs/use-cases/BACKLOG.md
§ orchestration

Where it sits on the rail

Each card is a step of /new-feature; the arrow carries the partial one step hands to the next. Dashed steps run only when the use case needs them.

/new-feature-implement Who reads the partial next

After approval, /new-feature-implement has the executor build the code. The spec is consolidated by reference and wins where it and a partial disagree; the partial is read, by section, for detail.

40-testes.mddocs/use-cases/UC-NNN-…/
read by section, for detail
java-spring-boot-developeragent · executor
writes the code
src/**designs one use case into a spec, writes no code
← back to the chains