Skip to content
Draft
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
10 changes: 5 additions & 5 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const sidebars = {
{ label: 'Start here', items: ['minimalapi/getting-started', 'minimalapi/guides/run-locally'] },
{
label: 'Guides',
items: ['minimalapi/guides/add-an-endpoint', 'minimalapi/guides/database', 'minimalapi/guides/secure-the-api', 'minimalapi/guides/ship-the-container', 'minimalapi/guides/set-up-ci'],
items: ['minimalapi/guides/add-an-endpoint', 'minimalapi/guides/test-your-changes', 'minimalapi/guides/database', 'minimalapi/guides/secure-the-api', 'minimalapi/guides/ship-the-container', 'minimalapi/guides/set-up-ci', 'minimalapi/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'minimalapi/reference' } }] },
{
Expand All @@ -24,7 +24,7 @@ const sidebars = {
{ label: 'Start here', items: ['webapi/getting-started', 'webapi/guides/run-locally'] },
{
label: 'Guides',
items: ['webapi/guides/add-an-endpoint', 'webapi/guides/database', 'webapi/guides/secure-the-api', 'webapi/guides/ship-the-container', 'webapi/guides/set-up-ci'],
items: ['webapi/guides/add-an-endpoint', 'webapi/guides/test-your-changes', 'webapi/guides/database', 'webapi/guides/secure-the-api', 'webapi/guides/ship-the-container', 'webapi/guides/set-up-ci', 'webapi/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'webapi/reference' } }] },
{
Expand All @@ -36,7 +36,7 @@ const sidebars = {
{ label: 'Start here', items: ['graphql/getting-started', 'graphql/guides/run-locally'] },
{
label: 'Guides',
items: ['graphql/guides/add-a-field', 'graphql/guides/database', 'graphql/guides/secure-the-api', 'graphql/guides/ship-the-container', 'graphql/guides/set-up-ci'],
items: ['graphql/guides/add-a-field', 'graphql/guides/test-your-changes', 'graphql/guides/database', 'graphql/guides/secure-the-api', 'graphql/guides/ship-the-container', 'graphql/guides/set-up-ci', 'graphql/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'graphql/reference' } }] },
{
Expand All @@ -48,7 +48,7 @@ const sidebars = {
{ label: 'Start here', items: ['mcp/getting-started', 'mcp/guides/run-locally'] },
{
label: 'Guides',
items: ['mcp/guides/connect-a-client', 'mcp/guides/add-a-tool', 'mcp/guides/database', 'mcp/guides/secure-the-server', 'mcp/guides/ship-the-container', 'mcp/guides/set-up-ci'],
items: ['mcp/guides/connect-a-client', 'mcp/guides/add-a-tool', 'mcp/guides/test-your-changes', 'mcp/guides/database', 'mcp/guides/secure-the-server', 'mcp/guides/ship-the-container', 'mcp/guides/set-up-ci', 'mcp/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'mcp/reference' } }] },
{
Expand All @@ -60,7 +60,7 @@ const sidebars = {
{ label: 'Start here', items: ['lambda/getting-started', 'lambda/guides/run-locally'] },
{
label: 'Guides',
items: ['lambda/guides/add-a-function', 'lambda/guides/database', 'lambda/guides/deploy-to-aws', 'lambda/guides/secure-the-function', 'lambda/guides/set-up-ci'],
items: ['lambda/guides/add-a-function', 'lambda/guides/test-your-changes', 'lambda/guides/database', 'lambda/guides/deploy-to-aws', 'lambda/guides/secure-the-function', 'lambda/guides/set-up-ci', 'lambda/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'lambda/reference' } }] },
{
Expand Down
2 changes: 2 additions & 0 deletions src/content/docs/graphql/explanation/how-its-built.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ sidebar:

import ReportCard from '../../../../components/docs/ReportCard.astro';

This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/graphql/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.

These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.

**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
Expand Down
74 changes: 74 additions & 0 deletions src/content/docs/graphql/guides/test-your-changes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: "Test your changes"
description: "Which test project a test belongs in, how the tests are written, and how to check the 95% coverage bar before CI does."
sidebar:
order: 5
---

import { Aside } from '@astrojs/starlight/components';

Your solution starts with four test projects and a pipeline that fails under 95% line coverage. That number isn't the goal, it's the floor. The goal is that every behaviour you add has a test that would fail if someone broke it, including you, six months from now.

## Where a test goes

| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | The error filter, cursors and settings | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | Real GraphQL requests for every query and mutation, authentication, health checks, and the schema snapshot | `WebApplicationFactory` with a private database |

Rule of thumb: test a rule where it lives. A validation rule gets a validator test, a query's filter gets a repository test, and an error code or a field's shape gets an integration test.

## What each kind of code needs

Every test has `// Arrange`, `// Act` and `// Assert` sections, tests one behaviour, and is named `Method_Should<Outcome>_<Condition>`, so a failure reads as a sentence.

| Code | Prove it works | Prove it says no |
| --- | --- | --- |
| Validator | A valid request has no errors, and each boundary value passes | One failing case per rule, asserting the message, and each value just outside a boundary |
| Handler | The happy path returns the result and passes the values and the cancellation token to the repository | Each documented failure throws the right exception, and nothing after it runs |
| Repository | The right rows come back or change, in order, with paging and search applied | Unknown ids return `null` or `false`, and nothing else changes |
| Query, mutation or type | One test per documented success | One test per documented failure, with the error checked |

For ranges, test both sides of each edge: stars `1` and `5` pass, `0` and `6` fail. A `[Theory]` with `[InlineData]` keeps that to one test.

```csharp
// tests/Application.Tests.Unit/Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs
[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(6)]
public void Validator_ShouldHaveValidationErrorFor_Stars(int stars)
```

## Run them

```bash
dotnet test
```

No test shares a database with another, so they run in parallel and never trip over each other. Nothing touches `movie-reviews.db` either, so your local data survives a test run.

## Check coverage before CI does

CI merges the coverage of all four projects into one total and fails under 95%. Run the same measurement from the solution root:

```bash
dotnet test -m:1 \
-p:CollectCoverage=true -p:CoverletOutput=../results/ -p:MergeWith="../results/coverage.json" \
-p:CoverletOutputFormat=opencover%2cjson \
-p:ExcludeByFile="**/Program.cs%2c**/Migrations/*.cs%2c**/obj/**/*.cs" \
-p:ExcludeByAttribute="GeneratedCodeAttribute"
```

The last table it prints is the merged total. `Program.cs`, the EF Core migrations and source-generated code are left out on purpose. Everything else counts.

<Aside type="caution" title="Under 95%?">
Write the test for the behaviour you skipped. `[ExcludeFromCodeCoverage]` is for wiring and plain data classes only, never for code that makes a decision. Lowering the number works exactly once, and then it's just a number.
</Aside>

## Where to next

- [Add a field](/graphql/guides/add-a-field/) ends with a test for the new field.
- [Work with a coding agent](/graphql/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
43 changes: 43 additions & 0 deletions src/content/docs/graphql/guides/work-with-a-coding-agent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: "Work with a coding agent"
description: "What AGENTS.md, Jacques and the skills give your coding agent, and how to get the most out of them."
sidebar:
order: 6
---

import { Aside } from '@astrojs/starlight/components';

Your solution ships with instructions for coding agents, the same ones we use to build the template. Point Claude Code, GitHub Copilot, Cursor, Codex or anything else that reads `AGENTS.md` at the solution, and it already knows where code goes, how it's tested and which checks must pass. You still decide what gets built. The agent just stops guessing how.

## What's in the box

| File | What it does |
| --- | --- |
| `AGENTS.md` | The map: what each project is for, the commands CI runs, the rules that fail a change, and a table of skills |
| `CLAUDE.md` | One line, `@AGENTS.md`, so Claude Code reads the same map |
| `.agents/agents/jacques.md` | Jacques, the sous-chef persona: does the prep, follows the recipes, proves every change and leaves the big calls to you |
| `.agents/skills/` | 33 skills, one folder each, covering every kind of change in the solution: use cases, validation, migrations, logging, telemetry, tests, coverage, Docker, CI and the GraphQL types, DataLoaders and the schema snapshot |

The skills are written for this solution, not for .NET in general. `add-a-use-case` shows the folder, the file names and the house style for a new command. `database-migrations` has the exact `dotnet ef` command. `coverage` has the same coverage command CI runs. Ask for something and the agent follows the recipe instead of improvising one.

## Get started

1. Open the solution in your agent of choice. Most read `AGENTS.md` without being asked. If yours doesn't, start with "Read AGENTS.md".
2. Ask for a change the way you'd ask a colleague: "Add a query that lists the reviews an author wrote, with paging, and put it on a query."
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.

## Make it yours

- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
- **Add a skill for a new pattern.** Bring in something the solution doesn't do yet, such as a message queue, and add a skill for it while it's fresh. `writing-a-skill` has the shape.
- **Rename Jacques if you like.** He won't mind. He's a sous-chef, and he's used to it.

<Aside type="tip">
The skills are plain Markdown. They're worth reading yourself: they're the house rules for this codebase, written down for once.
</Aside>

## What the agent won't do

`AGENTS.md` tells every agent never to push to `main` or merge a pull request, never to skip, delete or weaken a test or the coverage gate, never to suppress a warning to get a green build, never to add a package with a paid licence, and never to commit secrets. It stops and asks before changing the shared layers, adding a package, or changing anything a client depends on.
2 changes: 2 additions & 0 deletions src/content/docs/lambda/explanation/how-its-built.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ sidebar:

import ReportCard from '../../../../components/docs/ReportCard.astro';

This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/lambda/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.

These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.

**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
Expand Down
74 changes: 74 additions & 0 deletions src/content/docs/lambda/guides/test-your-changes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: "Test your changes"
description: "Which test project a test belongs in, how the tests are written, and how to check the 95% coverage bar before CI does."
sidebar:
order: 5
---

import { Aside } from '@astrojs/starlight/components';

Your solution starts with four test projects and a pipeline that fails under 95% line coverage. That number isn't the goal, it's the floor. The goal is that every behaviour you add has a test that would fail if someone broke it, including you, six months from now.

## Where a test goes

| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | The source-generated JSON serializer | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | Real calls to every function, through the handler and the classes Lambda Annotations generates | A `TestLambdaContext` and a private database |

Rule of thumb: test a rule where it lives. A validation rule gets a validator test, a query's filter gets a repository test, and what a caller gets back from a function gets an integration test.

## What each kind of code needs

Every test has `// Arrange`, `// Act` and `// Assert` sections, tests one behaviour, and is named `Method_Should<Outcome>_<Condition>`, so a failure reads as a sentence.

| Code | Prove it works | Prove it says no |
| --- | --- | --- |
| Validator | A valid request has no errors, and each boundary value passes | One failing case per rule, asserting the message, and each value just outside a boundary |
| Handler | The happy path returns the result and passes the values and the cancellation token to the repository | Each documented failure throws the right exception, and nothing after it runs |
| Repository | The right rows come back or change, in order, with paging and search applied | Unknown ids return `null` or `false`, and nothing else changes |
| Function | One test per documented success | One test per documented failure, with the error checked |

For ranges, test both sides of each edge: stars `1` and `5` pass, `0` and `6` fail. A `[Theory]` with `[InlineData]` keeps that to one test.

```csharp
// tests/Application.Tests.Unit/Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs
[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(6)]
public void Validator_ShouldHaveValidationErrorFor_Stars(int stars)
```

## Run them

```bash
dotnet test
```

No test shares a database with another, so they run in parallel and never trip over each other. Nothing touches the function's database either, so your local data survives a test run.

## Check coverage before CI does

CI merges the coverage of all four projects into one total and fails under 95%. Run the same measurement from the solution root:

```bash
dotnet test -m:1 \
-p:CollectCoverage=true -p:CoverletOutput=../results/ -p:MergeWith="../results/coverage.json" \
-p:CoverletOutputFormat=opencover%2cjson \
-p:ExcludeByFile="**/Program.cs%2c**/Migrations/*.cs%2c**/obj/**/*.cs" \
-p:ExcludeByAttribute="GeneratedCodeAttribute"
```

The last table it prints is the merged total. `Program.cs`, the EF Core migrations and source-generated code are left out on purpose. Everything else counts.

<Aside type="caution" title="Under 95%?">
Write the test for the behaviour you skipped. `[ExcludeFromCodeCoverage]` is for wiring and plain data classes only, never for code that makes a decision. Lowering the number works exactly once, and then it's just a number.
</Aside>

## Where to next

- [Add a function](/lambda/guides/add-a-function/) ends with a test for the new function.
- [Work with a coding agent](/lambda/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
43 changes: 43 additions & 0 deletions src/content/docs/lambda/guides/work-with-a-coding-agent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: "Work with a coding agent"
description: "What AGENTS.md, Jacques and the skills give your coding agent, and how to get the most out of them."
sidebar:
order: 6
---

import { Aside } from '@astrojs/starlight/components';

Your solution ships with instructions for coding agents, the same ones we use to build the template. Point Claude Code, GitHub Copilot, Cursor, Codex or anything else that reads `AGENTS.md` at the solution, and it already knows where code goes, how it's tested and which checks must pass. You still decide what gets built. The agent just stops guessing how.

## What's in the box

| File | What it does |
| --- | --- |
| `AGENTS.md` | The map: what each project is for, the commands CI runs, the rules that fail a change, and a table of skills |
| `CLAUDE.md` | One line, `@AGENTS.md`, so Claude Code reads the same map |
| `.agents/agents/jacques.md` | Jacques, the sous-chef persona: does the prep, follows the recipes, proves every change and leaves the big calls to you |
| `.agents/skills/` | 32 skills, one folder each, covering every kind of change in the solution: use cases, validation, migrations, logging, telemetry, tests, coverage, Docker, CI and the Lambda functions and the emulator |

The skills are written for this solution, not for .NET in general. `add-a-use-case` shows the folder, the file names and the house style for a new command. `database-migrations` has the exact `dotnet ef` command. `coverage` has the same coverage command CI runs. Ask for something and the agent follows the recipe instead of improvising one.

## Get started

1. Open the solution in your agent of choice. Most read `AGENTS.md` without being asked. If yours doesn't, start with "Read AGENTS.md".
2. Ask for a change the way you'd ask a colleague: "Add a query that lists the reviews an author wrote, with paging, and put it on a function."
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.

## Make it yours

- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
- **Add a skill for a new pattern.** Bring in something the solution doesn't do yet, such as a message queue, and add a skill for it while it's fresh. `writing-a-skill` has the shape.
- **Rename Jacques if you like.** He won't mind. He's a sous-chef, and he's used to it.

<Aside type="tip">
The skills are plain Markdown. They're worth reading yourself: they're the house rules for this codebase, written down for once.
</Aside>

## What the agent won't do

`AGENTS.md` tells every agent never to push to `main` or merge a pull request, never to skip, delete or weaken a test or the coverage gate, never to suppress a warning to get a green build, never to add a package with a paid licence, and never to commit secrets. It stops and asks before changing the shared layers, adding a package, or changing anything a client depends on.
Loading
Loading