When a check fails
Each check of npm run check says what failed, and where. This page shows the output of each check when it fails, and what to do. Fix the code, not the rule: the rules and the thresholds are in quality/, and a change there shows in the diff that the team reviews.
Read the output of npm run check
npm run check runs the six checks at once. It prints one line for each check, then the output of the checks that fail, and only theirs. Here, a run with four of the checks:
npx caits check lint:architecture typecheck lint:code test:coverage
4 checks at once: lint:architecture, typecheck, lint:code, test:coverage
✗ lint:architecture 0s
✓ lint:code 3s
✓ typecheck 3s
✓ test:coverage 4s
── lint:architecture ──
src/backend/messages/application/command/add-message/add-message.handler.ts → src/backend/messages/infra/message-repository/file-message-repository.js: application must not depend on infra (layer-direction)
1 architecture violation.
1 of 4 checks failed: lint:architecture.To fix one check, run it alone: it is faster.
| Check | Run it alone | What it checks |
|---|---|---|
typecheck | npm run typecheck | The types of the code and of the Marko templates, after a build |
lint:code | npm run lint:code | ESLint |
lint:architecture | npm run lint:architecture | The 52 architecture rules, in less than a second |
test:coverage | npm run test:coverage | The unit tests, and 100% coverage |
test:mutation | npm run test:mutation | The mutation tests, and 100% of the mutants killed |
test:e2e | npm run test:e2e | The end-to-end tests, in a browser, on the production build |
An architecture rule fails
Each line names the file, the import or the place, the message, and the rule in parentheses:
npm run lint:architecture
src/backend/messages/application/command/add-message/add-message.handler.ts → src/backend/messages/infra/message-repository/file-message-repository.js: application must not depend on infra (layer-direction)Find the rule by its name in the 52 rules: each one has its description. The most frequent ones:
| Rule | What it means | The usual fix |
|---|---|---|
layer-direction | A layer imports a layer that it must not know | Use the port of the domain, not the adapter of the infra |
imports-by-shortcut | An import goes up a folder (../) | Use #context/, #shared/, #backend/ or #frontend/ |
contexts-isolated | A context imports another context | Publish an event, and connect the contexts in the context map |
port-has-contract | A port has no contract | Write <Port>.contract.ts next to the port |
adapter-test-runs-contract | The test of an adapter does not run the contract of its port | Call describe<Port>Contract(…) in the test of the adapter |
dtos-sent-by-store | A view imports a DTO | The view publishes an event; the store sends the DTO |
A dependency is missing
CAITS builds each handler and each controller with its dependencies. When one is missing, the error tells what to do. The backend checks them all when it starts; a frontend context, when a page builds it.
| The error says | Do this |
|---|---|
Nothing is bound to AddMessageHandler → MessageRepository | Bind MessageRepository in the di.ts of the context, or in test.di.ts for the tests |
Nothing is bound to CountWordsHandler → WordCountRepository → DataDirectory | A second context keeps files: move the binding of DataDirectory from the di.ts of the first context to src/backend/shared/di.ts, which gives it to every backend context |
… needs a value for its parameter limit: its type Limit has no token (export const Limit = new Token<Limit>()), or give it with bind(Gauge).with({ limit: … }) | Add the token next to the interface, or give the value in the binding |
… needs a value for its parameter fileName: give it with bind(MessageRepository).to(FileMessageRepository).with({ fileName: … }) | Give the value of the parameter in the binding |
No context owns CountMessagesQuery | Write the handler, next to the DTO: <use-case>.handler.ts |
No handler subscribes to … | The same: the DTO has no handler in its folder |
… declares one input DTO (found 2) | Keep one Input DTO in each DTO file: one use case per folder |
No context binds MessageRepository | In a test, app.get or app.use asks for a port that no di.ts or test.di.ts binds |
No frontend context is named … | The name in frontendOf($global, "…") must be the name of the folder of the context, and of its entry in Frontends |
A test frontend has no network: replace the dependency that asked for /api/… | An adapter that takes the Fetcher asked for the API in a test. Bind a fake adapter of its port in the test.di.ts of the frontend context |
The coverage is under 100%
npm run test:coverage
ERROR: Coverage for branches (97.5%) does not meet global threshold (100%)The table above this line names the file and the lines that no test runs. Write the test that runs them, or remove the code that nothing needs. The report reports/coverage/index.html shows each line in color.
A mutant survived
A mutant that survives is a change of the code that no test sees. When the score is under 100%, Stryker says Final mutation score … under breaking threshold 100, and suggests changing thresholds.break: do not, fix the tests. Then npm run test:mutation ends with the score of each file and the mutants that survived, the lowest score first. npx caits mutation-summary --survivors gives them again, from the last report. Here, one test was removed from the example app, and Stryker ran on Message.ts only:
npx caits mutation-summary --survivors
Mutation score: 91.7% (11 killed of 12 valid mutants)
92% src/backend/messages/domain/entity/Message.ts (killed 11, survived 1, no coverage 0)
L16 [survived] EqualityOperator: if ([...trimmed].length > Message.MAX_LENGTH) throw new InvalidMessageError(`A message has → [...trimmed].length >= Message.MAX_LENGTHHere, no test sends a message of exactly 280 characters: > and >= give the same results for all the tests. Write this test, and the mutant dies.
When no test can see a mutant, because the two versions of the code do the same thing, tell Stryker why, on the line above:
A mutant that no test can see
// Stryker disable next-line EqualityOperator: the two operators give the same result for an index that is never negativeA reviewer reads the reason in the diff. Use it rarely: most survivors are a missing test.
A type is wrong
npm run typecheck builds the app, then checks the types of the TypeScript files and of the Marko templates. Each error names the file, the line, and the column. The build comes first: the types of the routes come from it.
An end-to-end test fails
The output names the test, the step that failed, and what the page showed instead. Playwright keeps what it saw in test-results/: an error-context.md with the content of the page at the failure. The end-to-end tests run on the production build, on the port 4310: a test that passes with npm run dev can fail there, for example when the build misses a file.
On Linux, the browser of the end-to-end tests does not start when a library of the system is missing. Then install the browser with the packages of the system that it needs:
Terminal
npx playwright install --with-deps chromiumTo run some tests only, give a part of their name:
Terminal
npm run test:e2e -- -g "a message"