# Architecture Tests

Course: [Harness Engineering](https://www.devtutorials.ai/learn/harness-engineering) · Section: Scale and Maintain
Source: https://www.devtutorials.ai/learn/harness-engineering/scale-maintain/invariants-and-garbage-collection

> Why an agent takes a shortcut through your layers while every test stays green, how an architecture test turns a layer rule into a check that teaches the fix, and how a doc-gardening skill finds stale docs on a schedule without deleting them.

## Architecture Tests

Agents add code every day. Bit by bit, the code drifts from its plan.

**Layer** = a group of files that have one job.

Both lab projects have layers:

- In the React Taskboard, `src/components` shows things on the screen.
- `src/services` gets the data for the screen.
- `src/db` talks to the database.
- In the ASP.NET Core Pricing API, the same three are `Pricing.Api.Controllers`, `Pricing.Api.Services` and `Pricing.Api.Data`.

Components should get their data through the services. They should never read the database directly.

This lesson shows two ways to stop the drift.

An **architecture test** guards the layers. A **doc gardener** finds docs that are no longer true.

### A Real-World Example

Think about a restaurant.

Guests order from the waiter. The waiter talks to the kitchen.

Guests do not walk into the kitchen.

One day, a guest is in a hurry. They walk into the kitchen and take their food.

The food is right.

But next time, ten guests will do the same thing.

A sign that says "No entry" helps a little.

A sign that says "No entry, please ask your waiter" helps more.

It says what to do instead.

The menu on the wall also goes out of date. Someone checks it every week.

They do not tear it down. They tell the manager what is wrong.

Agents need both:

1. A sign on the door. That is the architecture test.
2. A weekly walk. That is the doc gardener.

Click the button and open the **Sessions & App** tab.

- **The edit:** the agent adds an import from the database to a UI file.
- **The test run:** every test passes.
- **The App tab:** it says nothing caught the shortcut.

**Simple lesson:** green tests do not mean the layers are kept.

## What You Will Learn

In this lesson, you will learn:

1. What an invariant is.
2. Why unit tests miss a shortcut through your layers.
3. How to write an architecture test whose message teaches the fix.
4. Why the rule must point in the right direction.
5. What garbage collection means for a codebase.
6. How to write a doc-gardening skill that checks paths and commands, and only reports.

**Lab vs real tools**

- The lab's agent is simulated, so it does the same thing every run. That is how your work can be checked.
- A real agent is less predictable. Try each idea on your own project too.

## Invariants: Rules That Must Always Hold

### Why the Tests Stay Green

A unit test asks one thing. Does this code give the right answer?

A layer shortcut still gives the right answer. The header still shows the ticket count.

So the tests pass. The damage is in the shape of the code.

Over many sessions, the shortcuts pile up.

Soon every component talks to the database. Then nobody can change the database safely.

Step through all four frames.

- **Frame 1:** the three layers and the job.
- **Frame 2:** the real file the agent wrote. Its import skips the service layer.
- **Frame 3:** the path from the UI goes around the services.
- **Frame 4:** the agent's real run. The tests pass and nothing complains.

### What an Invariant Is

**Invariant** = a rule that must be true after every change.

Here are two:

- "Components never import `src/db`."
- "Controllers never use `Pricing.Api.Data`."

The harness team at OpenAI calls these **golden principles**. They write them down.

Then they enforce them with checks.

### The Architecture Test

**Architecture test** = a test that reads which file uses which, and fails when one layer uses a layer it must not use.

Here it is for each project. React keeps it in `src/architecture.test.ts`. ASP.NET Core keeps it in `Pricing.Tests/ArchitectureTests.cs`.

```text
import { forbidImport } from "./test-utils/architecture";

forbidImport("src/components", "src/db", "Components must not read the database. Go through src/services instead.");
```

```text
using Xunit;

public class ArchitectureTests
{
    [Fact]
    public void ControllersDoNotUseData() =>
        Arch.ForbidDependency("Pricing.Api.Controllers", "Pricing.Api.Data", "Controllers must not use the database directly. Go through Pricing.Api.Services instead.");
}
```

Each call has three parts:

1. The layer.
2. The layer it must not use.
3. A message.

Services may still use the database. Only the UI layer is blocked.

### The Message Must Name the Way

A message like "Forbidden dependency" only stops the agent.

"Go through `src/services` instead" lets the agent fix the code by itself.

Step through all five frames.

- **Frame 1:** the one line you add.
- **Frame 2:** the agent takes the shortcut. The architecture test fails with your message.
- **Frame 3:** the agent switches to the service layer. The tests pass.
- **Frame 4:** the same test with the "Go through" part cut off. The agent stops, blocked.
- **Frame 5:** all three versions of the job, fixed by the agent.

### The Direction Matters

`forbidImport("src/db", "src/components", ...)` is a different rule. It says the database must not use components.

That is true too. But it does not catch this agent.

Write the rule as "this layer must not use that layer". Use the same direction as the shortcut.

**Copy the layer names from the Files tab.**

A typo such as `src/component` matches nothing.

Then the test passes and catches nothing.

### Lab Exercise: Turn the Rule Into a Test

Open `src/architecture.test.ts` or `Pricing.Tests/ArchitectureTests.cs`. Write one call with three things: the UI layer, the database layer and a message that names the way.

Click the button, write the call, then press **Check my harness**.

- **The check:** it runs all 3 versions of the job.
- **To pass:** the test must catch the shortcut every time.
- **And:** the agent must fix the code by itself, using your message.

**Simple lesson:** the test catches, and the message teaches.

## Garbage Collection: Cleanup That Runs Again and Again

### Docs Go Stale

Code changes every day. Docs do not change by themselves.

**Stale doc** = an old doc that is no longer true.

Say a doc says "all calls go through `src/api/legacyClient.ts`". That file is gone.

The agent reads the doc, trusts it and goes looking for it.

In a programming language, garbage collection frees memory that nobody uses. It runs again and again in the background.

**Garbage collection** (for a codebase) = a repeating job that finds what has drifted and reports it.

The OpenAI team runs a **doc-gardening agent** for this.

### Two Ways a Doc Goes Stale

The lab has 6 docs for each project. 3 of them are stale.

| Stack | Doc | Why it is stale |
|---|---|---|
| React | `docs/old-api.md` | names `src/api/legacyClient.ts`, which is gone |
| React | `docs/theme.md` | names `src/styles/legacy-theme.css`, which is gone |
| React | `docs/testing.md` | names `pnpm test:unit`, a command that is gone |
| .NET | `docs/old-api.md` | names `Pricing.Api/Legacy/RuleEngine.cs`, which is gone |
| .NET | `docs/cache.md` | names `Pricing.Api/Infrastructure/OldCache.cs`, which is gone |
| .NET | `docs/testing.md` | names `./run-tests.ps1`, a script that is gone |

So a gardener must check two things: **paths** and **commands**.

### Report, Never Delete

A stale doc may still hold one true sentence. So the gardener does not delete or rewrite anything.

It reports each stale doc and says why. It writes one pull request description.

Then a person decides.

Here is the gardening skill, as `garden-docs/SKILL.md` in your tool's skills folder:

```markdown
---
name: garden-docs
description: Use when someone asks to clean up, find or fix stale docs that no longer match the code.
---

1. List every file in docs/.
2. For each file path a doc mentions, check that the file exists.
3. For each command a doc mentions, check it is a real script in package.json or a real dotnet command.
4. Do not delete or rewrite docs. Report each stale doc and why, as one pull request description.
```

Step through all five frames.

- **Frame 1:** the 6 real docs and what each one names.
- **Frame 2:** the starting skill. Its description is too vague to load.
- **Frame 3:** a skill that checks paths only. It misses `testing.md`.
- **Frame 4:** add the command step. All 3 stale docs show up.
- **Frame 5:** the same skill with "Delete each stale doc." It finds the same docs and destroys them.

**"Clean up" often turns into "delete".**

Write the "do not delete" step into the skill in plain words.

The lab grader fails any skill whose steps say delete, remove or rewrite without a "do not" in front.

### Lab Exercise: A Doc-Gardening Skill

The starting skill says `description: Helps with docs.` and has one step. Fix it so that:

1. It loads for "Clean up stale docs that no longer match the code."
2. It has one step that checks file paths and one step that checks commands.
3. It only reports. It never deletes.

Click the button, edit the skill, then press **Check my harness**.

- **To pass:** the skill finds exactly the 3 stale docs.
- **And:** it changes nothing.

**Simple lesson:** use the words of the request in the description, and say "do not delete".

## Where Invariants and Gardening Fit

### Planning and Scheduling

Write each invariant once, when you plan the layers.

Then schedule the gardener. Many teams run it once a week.

CI (see **Tests and Linters**) runs checks on every change.

Run the gardener in CI or as a scheduled agent job. It opens one pull request with its report.

### Frontend and Backend

- **Frontend:** forbid `src/components` → `src/db`. Use dependency-cruiser or eslint-plugin-boundaries.
- **Backend:** forbid `Pricing.Api.Controllers` → `Pricing.Api.Data`. Use NetArchTest or ArchUnitNET in an xUnit test.

### Testing and Integration

Put the architecture test in the normal test run: `pnpm test` or `dotnet test`.

Then it is part of the definition of done. Every session sees it.

The gardener's report goes to the team. It does not go straight into the main branch.

**Simple lesson:** the architecture test runs with every test run, so no session can skip it.

## TL;DR

1. **Layer** = a group of files with one job.
2. **Invariant** = a rule that must be true after every change.
3. **Architecture test** = a test that fails when one layer uses a layer it must not use.
4. **Message** = it must name the right way, such as "Go through `src/services` instead".
5. **Direction** = forbid the same arrow the shortcut takes.
6. **Stale doc** = a doc that names a file or a command that is gone.
7. **Garbage collection** = cleanup that runs again and again, on a schedule.
8. **Doc gardener** = a skill that checks paths and commands, and only reports.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No architecture test | "The tests are green, so the code is fine." | Tests check output. An architecture test checks the layers. | Shortcuts pile up, session after session. |
| Message without the way | "Forbidden dependency." | "Go through `src/services` instead." | The agent stops, blocked. |
| Rule in the wrong direction | `forbidImport("src/db", "src/components", ...)` | `forbidImport("src/components", "src/db", ...)` | The test passes and catches nothing. |
| Gardener deletes docs | "Clean up means remove." | Report each stale doc and why. | Useful text is lost, and nobody reviewed it. |
| Only checks paths | "Stale means a missing file." | Check paths and commands. | `pnpm test:unit` stays in the docs. |
| Vague skill description | "Helps with docs." | Use the words of the request. | The skill never loads. |

## Check Your Understanding

## Exercises

These exercises practise architecture tests and doc gardening.

Press **Check** when you are ready. Use **Show Hint** if you get stuck.

## What to Remember

- An invariant is a rule that must always hold. Write it down, then enforce it.
- Unit tests miss layer shortcuts. An architecture test catches them.
- The message must name the right way. The rule must point the same way as the shortcut.
- Garbage collection is cleanup that repeats. A doc gardener checks paths and commands on a schedule.
- A gardener reports. A person decides.

## Further Reading

- [Harness engineering: leveraging Codex in an agent-first world, by OpenAI](https://openai.com/index/harness-engineering/): golden principles, architecture checks and a doc-gardening agent.
- [Harness engineering for coding agent users, on martinfowler.com](https://martinfowler.com/articles/harness-engineering.html): keeping the codebase fit for agents over time.
- [awesome-harness-engineering, by walkinglabs](https://github.com/walkinglabs/awesome-harness-engineering): more checks and maintenance patterns.

Next: **Continuous Improvement**. You turn each mistake into one check and remove the parts that do nothing.
