@cratis/pi 0.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -37
- package/package/corpus/agents/backend-developer.md +125 -0
- package/package/corpus/agents/code-reviewer.md +165 -0
- package/package/corpus/agents/coordinator.md +163 -0
- package/package/corpus/agents/frontend-developer.md +246 -0
- package/package/corpus/agents/orchestrator.md +196 -0
- package/package/corpus/agents/performance-reviewer.md +109 -0
- package/package/corpus/agents/planner.md +145 -0
- package/package/corpus/agents/repository-investigation-reviewer.md +45 -0
- package/package/corpus/agents/repository-investigator.md +50 -0
- package/package/corpus/agents/security-reviewer.md +118 -0
- package/package/corpus/agents/slice-implementer.md +59 -0
- package/package/corpus/agents/spec-writer.md +149 -0
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +213 -0
- package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +27 -0
- package/package/corpus/harnesses/pi/extensions/package.json +4 -0
- package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +167 -0
- package/package/corpus/harnesses/pi/extensions/subagent/index.ts +352 -0
- package/package/corpus/hooks/README.md +434 -0
- package/package/corpus/hooks/agent-stop.md +49 -0
- package/package/corpus/hooks/pre-commit.md +47 -0
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +87 -0
- package/package/corpus/hooks/scripts/cratis-nuget-pins.txt +11 -0
- package/package/corpus/hooks/scripts/cratis-pattern-scan.sh +197 -0
- package/package/corpus/hooks/scripts/cratis-patterns.json +84 -0
- package/package/corpus/hooks/scripts/cratis-quality-gate.sh +219 -0
- package/package/corpus/hooks/scripts/hook-lib.sh +152 -0
- package/package/corpus/hooks/scripts/quality-gates.json +261 -0
- package/package/corpus/hooks/scripts/type-references-allowlist.txt +71 -0
- package/package/corpus/hooks/scripts/validate-package-imports.sh +166 -0
- package/package/corpus/hooks/scripts/validate-package-subpaths.sh +120 -0
- package/package/corpus/hooks/scripts/validate-type-references.sh +308 -0
- package/package/corpus/hooks/settings.template.json +40 -0
- package/package/corpus/prompts/add-business-rule.prompt.md +22 -0
- package/package/corpus/prompts/add-concept.prompt.md +17 -0
- package/package/corpus/prompts/add-ef-migration.prompt.md +24 -0
- package/package/corpus/prompts/add-projection.prompt.md +20 -0
- package/package/corpus/prompts/add-reactor.prompt.md +22 -0
- package/package/corpus/prompts/add-reducer.prompt.md +20 -0
- package/package/corpus/prompts/audit-hooks.prompt.md +15 -0
- package/package/corpus/prompts/check-doc-drift.prompt.md +21 -0
- package/package/corpus/prompts/code-review.prompt.md +9 -0
- package/package/corpus/prompts/new-feature.prompt.md +9 -0
- package/package/corpus/prompts/new-vertical-slice.prompt.md +18 -0
- package/package/corpus/prompts/review-pr.prompt.md +35 -0
- package/package/corpus/prompts/review-skill.prompt.md +16 -0
- package/package/corpus/prompts/scaffold-feature.prompt.md +16 -0
- package/package/corpus/prompts/ship-changes.prompt.md +20 -0
- package/package/corpus/prompts/verify-ai-setup.prompt.md +19 -0
- package/package/corpus/prompts/write-documentation.prompt.md +21 -0
- package/package/corpus/prompts/write-specs.prompt.md +22 -0
- package/package/corpus/rules/capability-is-not-authority.md +31 -0
- package/package/corpus/rules/code-quality.csharp.md +91 -0
- package/package/corpus/rules/code-quality.md +82 -0
- package/package/corpus/rules/code-quality.typescript.md +89 -0
- package/package/corpus/rules/components.md +207 -0
- package/package/corpus/rules/concepts.md +115 -0
- package/package/corpus/rules/csharp.md +269 -0
- package/package/corpus/rules/dialogs.md +264 -0
- package/package/corpus/rules/documentation-structure-and-formatting.md +148 -0
- package/package/corpus/rules/documentation.md +90 -0
- package/package/corpus/rules/editing-cratis-docs.md +69 -0
- package/package/corpus/rules/efcore.md +235 -0
- package/package/corpus/rules/efcore.specs.md +44 -0
- package/package/corpus/rules/exit-codes-and-wrappers.md +33 -0
- package/package/corpus/rules/framework.md +52 -0
- package/package/corpus/rules/frontend-quality.md +59 -0
- package/package/corpus/rules/frontend-testing.md +126 -0
- package/package/corpus/rules/general.md +305 -0
- package/package/corpus/rules/git-commits.md +138 -0
- package/package/corpus/rules/github-actions.md +92 -0
- package/package/corpus/rules/glossary.md +61 -0
- package/package/corpus/rules/guards-and-fuses.md +45 -0
- package/package/corpus/rules/local-work-artifacts.md +33 -0
- package/package/corpus/rules/managing-ai-rules.md +40 -0
- package/package/corpus/rules/orleans.md +50 -0
- package/package/corpus/rules/pull-requests.md +78 -0
- package/package/corpus/rules/react.md +195 -0
- package/package/corpus/rules/reactors.md +238 -0
- package/package/corpus/rules/rtk.md +37 -0
- package/package/corpus/rules/specs.csharp.md +139 -0
- package/package/corpus/rules/specs.md +132 -0
- package/package/corpus/rules/specs.scenarios.csharp.md +172 -0
- package/package/corpus/rules/specs.typescript.md +139 -0
- package/package/corpus/rules/storybook.md +84 -0
- package/package/corpus/rules/terminal-commands.md +19 -0
- package/package/corpus/rules/typescript.md +149 -0
- package/package/corpus/rules/verification-discipline.md +21 -0
- package/package/corpus/rules/vertical-slices.md +338 -0
- package/package/corpus/rules/web-fetching.md +11 -0
- package/package/corpus/rules/writing-correct-examples.md +35 -0
- package/package/corpus/rules/writing-cratis-docs.md +70 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/SKILL.md +131 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/code-style.md +187 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/domain-philosophy.md +91 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/exceptions-logging-and-di.md +223 -0
- package/package/corpus/skills/cratis-engineering-decision-record/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-decision-record/SKILL.md +133 -0
- package/package/corpus/skills/cratis-engineering-decision-record/references/record-format.md +107 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +86 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/references/site-format.md +46 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/SKILL.md +130 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/references/failure-archetypes.md +133 -0
- package/package/corpus/skills/cratis-fundamentals-concept/verification.json +8 -0
- package/package/profile-catalog.json +821 -0
- package/package.json +35 -13
- package/src/index.ts +80 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authentication.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authorization.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/frontend.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/local-development.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/command-result.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/handler-shapes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/proxy-generation.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/read-model-injection.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-page.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-tables.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/dialogs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/mvvm.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/queries-and-commands.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/observational-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/fluent-builder.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/model-bound-attributes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/references/queries.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/application-scenarios.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/csharp-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/integration-specs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/references/typescript-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/observational-tools.md +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/for_*/**/*.*, **/when_*/**/*.*"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/for_*/**/*.*"
|
|
5
|
+
- "**/when_*/**/*.*"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# How to Write Specs
|
|
9
|
+
|
|
10
|
+
We call automated tests **specs** (specifications), not tests. This is deliberate — specs are executable documentation that describe what the system *does*, written in the language of the domain. A new developer should be able to read the spec folder structure like a table of contents and understand the system's behavior without opening a single source file.
|
|
11
|
+
|
|
12
|
+
This philosophy comes from Specification by Example and BDD (Behavior Driven Development). The goal is human-readable, navigable specifications that double as a living contract.
|
|
13
|
+
|
|
14
|
+
## Core Philosophy
|
|
15
|
+
|
|
16
|
+
- **Specify behaviors, not implementations.** A spec should verify what a method *promises from its signature* — its contract with callers. If the implementation changes but the contract holds, specs should still pass. When they don't, the spec was testing the wrong thing.
|
|
17
|
+
- **One behavior, one spec.** Every public method that performs an action gets its own `when_` folder or file. Never bundle multiple behaviors into one spec — it obscures what broke and why.
|
|
18
|
+
- **Specs are documentation first.** The folder tree, class names, and `should_` assertions form a specification anyone can read. Optimize for readability over DRY. A little repetition in setup is fine if it makes the spec self-contained and clear.
|
|
19
|
+
- **Do not test logging** — it is too fragile and provides no value. Don't test simple delegation or trivial getters either. The cost of maintaining these specs exceeds the value they provide.
|
|
20
|
+
|
|
21
|
+
## Folder & File Structure
|
|
22
|
+
|
|
23
|
+
Specs mirror the source structure and read like a sentence when you trace the path: `for_Changeset / when_adding_changes / and_there_are_differences`. This is not accidental — the folder hierarchy *is* the specification. Every level adds context:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
for_<TypeUnderTest>/
|
|
27
|
+
├── given/
|
|
28
|
+
│ ├── all_dependencies ← common DI/mock setup
|
|
29
|
+
│ └── a_<descriptive_name> ← reusable context
|
|
30
|
+
├── when_<behavior>/ ← folder for behaviors with multiple outcomes
|
|
31
|
+
│ ├── given/ ← behavior-specific context (optional)
|
|
32
|
+
│ │ └── a_<specific_setup>
|
|
33
|
+
│ ├── and_<condition>.cs ← spec file for one outcome
|
|
34
|
+
│ ├── and_<condition>/ ← OR a sub-folder when that condition itself has multiple outcomes
|
|
35
|
+
│ │ ├── with_<data_state>.cs
|
|
36
|
+
│ │ └── without_<requirement>.cs
|
|
37
|
+
│ ├── with_<data_state>.cs
|
|
38
|
+
│ └── without_<requirement>.cs
|
|
39
|
+
└── when_<simple_behavior>.cs ← single file for single-outcome behaviors
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `and_`, `with_`, `without_`, `having_`, `given_` prepositions work **both** as file names and as folder names. Use a folder when there are multiple outcomes under that condition; use a file when there is only one.
|
|
43
|
+
|
|
44
|
+
**Naming conventions — read them as English sentences:**
|
|
45
|
+
| Element | Pattern | Reads as... |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Unit folder | `for_<ClassName>` | "For the Changeset..." |
|
|
48
|
+
| Behavior folder | `when_<action>` | "...when adding changes..." |
|
|
49
|
+
| Condition folder or spec file | Descriptive preposition | "...and there are differences" |
|
|
50
|
+
| Assertion | `should <expected result>` | "...it should return true" |
|
|
51
|
+
|
|
52
|
+
**Allowed prepositions for spec file/class names (and sub-folder names):**
|
|
53
|
+
- `and_*` — additional conditions or compound scenarios
|
|
54
|
+
- `with_*` / `without_*` — specific data or state present/absent
|
|
55
|
+
- `having_*` — possession or state-based conditions
|
|
56
|
+
- `given_*` — precondition scenarios
|
|
57
|
+
|
|
58
|
+
**Critical naming rule — never embed `when` in a spec file or folder name:**
|
|
59
|
+
`when` belongs **only** in `when_<behavior>` folder names. A spec file, spec class, or non-`when_` folder must **never** contain the word `when` anywhere in it. If the name starts with a preposition (`with_`, `and_`, etc.) but also contains `_when_` somewhere in the middle (e.g. `with_a_registered_migration_when_appending_a_generation_1_event`), you have two "whens" in the sentence — which is always wrong.
|
|
60
|
+
|
|
61
|
+
The fix is to fold the context into the `when_` folder name itself, then use preposition files/folders for the outcomes:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
# ❌ Wrong — double when in the sentence path
|
|
65
|
+
when_appending_event_with_migrations/
|
|
66
|
+
└── with_a_registered_migration_when_appending_a_generation_1_event.cs
|
|
67
|
+
|
|
68
|
+
# ❌ Still wrong — unnecessary extra level when a single flat file suffices
|
|
69
|
+
when_appending_event_with_migrations/
|
|
70
|
+
└── and_event_is_generation_1/
|
|
71
|
+
└── with_a_registered_migration.cs
|
|
72
|
+
|
|
73
|
+
# ✅ Correct — context baked into the when_ folder; outcomes are flat files
|
|
74
|
+
when_appending_event_with_registered_migration/
|
|
75
|
+
├── and_event_is_generation_1.cs
|
|
76
|
+
├── and_event_is_generation_2.cs
|
|
77
|
+
└── and_event_has_default_value.cs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A sub-folder under `when_` is only needed when that condition has its **own** multiple outcomes that warrant further breakdown. If there is only one outcome per condition, use a flat file. This applies to **all languages** (C#, TypeScript, etc.).
|
|
81
|
+
|
|
82
|
+
## What to Specify
|
|
83
|
+
|
|
84
|
+
The goal is to cover *decisions and transformations* — code where bugs hide. Simple plumbing that the compiler already validates is noise.
|
|
85
|
+
|
|
86
|
+
**Write specs for:**
|
|
87
|
+
- Public methods that perform actions (behaviors)
|
|
88
|
+
- Methods with branching logic or business rules
|
|
89
|
+
- Methods that coordinate between dependencies
|
|
90
|
+
|
|
91
|
+
**Do NOT write specs for:**
|
|
92
|
+
- Simple auto-properties (`public string Name { get; set; }`)
|
|
93
|
+
- Properties returning constructor parameters (`public Key Key => key;`)
|
|
94
|
+
- Simple delegation (`public IEnumerable<Property> Properties => mapper.Properties;`)
|
|
95
|
+
- Trivial null checks or basic validation without complex logic
|
|
96
|
+
- Getters returning injected dependencies
|
|
97
|
+
|
|
98
|
+
**Avoid file names starting with:** `when_getting_*`, `when_returning_*` — if a spec name starts with "getting" or "returning", it's probably testing a simple getter, which is not worth specifying.
|
|
99
|
+
|
|
100
|
+
## Multiple Outcomes
|
|
101
|
+
|
|
102
|
+
Each distinct outcome deserves its own spec file. This keeps specs small, focused, and independently verifiable. When a spec fails, you immediately know *which* outcome broke — no debugging through a multi-assertion file.
|
|
103
|
+
|
|
104
|
+
- When a behavior has multiple outcomes, create a `when_<behavior>/` folder with separate files for each outcome.
|
|
105
|
+
- For simple behaviors with a single outcome, use a single file: `when_<behavior>`.
|
|
106
|
+
- Never write a single file that tests an entire class.
|
|
107
|
+
|
|
108
|
+
## Reusable Context
|
|
109
|
+
|
|
110
|
+
Contexts capture the "given" part of a specification — the world as it exists before the action under test. They prevent duplicating setup across specs while keeping each spec readable.
|
|
111
|
+
|
|
112
|
+
- Place in `given/` folder within the unit folder.
|
|
113
|
+
- Name with `a_` or `an_` prefix (e.g. `an_observer`, `a_command_pipeline`). This reads naturally: "given an observer, when handling..."
|
|
114
|
+
- More specific contexts can use descriptive names (e.g. `two_queries`, `existing_query`).
|
|
115
|
+
- Context properties must be accessible to the specs that use them — see language-specific instructions for the exact access modifiers and naming conventions.
|
|
116
|
+
- Use the setup phase for context initialization — **never** put the action under test in a reusable context. The action under test belongs only in the concrete spec.
|
|
117
|
+
- Contexts can build on each other in layers: `all_dependencies → a_reactor_handler → when_handling`.
|
|
118
|
+
- Consider creating `all_dependencies` as a root context that mocks all common dependencies. This avoids duplicating mock creation across unrelated specs.
|
|
119
|
+
|
|
120
|
+
## Formatting
|
|
121
|
+
|
|
122
|
+
- Keep assertions concise — prefer single-line assertions where the logic is readable.
|
|
123
|
+
- Don't add blank lines between related assertions for the same behavior.
|
|
124
|
+
|
|
125
|
+
## Language-specific guides
|
|
126
|
+
|
|
127
|
+
This file is the shared base. For the concrete patterns:
|
|
128
|
+
|
|
129
|
+
- [specs.csharp.md](./specs.csharp.md) — C#: the universal `Cratis.Specifications` base + NSubstitute (both profiles; this is what framework/library specs use).
|
|
130
|
+
- [specs.scenarios.csharp.md](./specs.scenarios.csharp.md) — C# **application** profile: the in-process scenario family (`CommandScenario`, `EventScenario`, `ReadModelScenario`, `ReactorScenario`) + out-of-process Chronicle integration.
|
|
131
|
+
- [specs.typescript.md](./specs.typescript.md) — TypeScript framework-package specs (`given()` helper).
|
|
132
|
+
- [frontend-testing.md](./frontend-testing.md) — application frontend specs (view models, React components).
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/for_*/**/*.cs"
|
|
5
|
+
- "**/when_*/**/*.cs"
|
|
6
|
+
profile: application
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Application Specs — the in-process scenario family
|
|
10
|
+
|
|
11
|
+
> **Application profile.** This file covers spec-writing for **event-sourced Cratis applications** — exercising commands, projections, reducers, reactors, and constraints with the in-process scenario family. It builds on the universal [specs.csharp.md](./specs.csharp.md) (the `Specification` base + NSubstitute). **Framework / library** specs predominantly use that plain base instead; a framework repo reaches for a scenario helper here only when testing the very engine it provides — Arc tests its command pipeline with `CommandScenario`, Chronicle tests its event/projection/reactor engine with `EventScenario`/`ReadModelScenario`/`ReactorScenario`. That is the minority case, not the general framework testing mode.
|
|
12
|
+
|
|
13
|
+
Specs are **mandatory for every slice type**, including reactors.
|
|
14
|
+
|
|
15
|
+
## Lead with the in-process scenario family
|
|
16
|
+
|
|
17
|
+
For Cratis application behavior, prefer the four in-process scenario helpers over full out-of-process Chronicle host specs. They target different concerns and are additive.
|
|
18
|
+
|
|
19
|
+
| Tool | Tests | Use when |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `Specification` (unit) | an isolated class with mocked collaborators | pure function / injected services to mock |
|
|
22
|
+
| `CommandScenario<TCommand>` | the real Arc command pipeline — authorization + validators + `Provide()` + `Handle()` + appended events | **default for State Change slices** |
|
|
23
|
+
| `EventScenario` | Chronicle-level append semantics, no command pipeline | constraint violations, raw stream sequencing/concurrency |
|
|
24
|
+
| `ReadModelScenario<TReadModel>` | projection/reducer state from a sequence of events (auto-detects model-bound / fluent / reducer) | **default for State View slices** |
|
|
25
|
+
| `ReactorScenario<TReactor>` | reactor handler invocation + side effects via mocked services | Automation / Translation slices |
|
|
26
|
+
|
|
27
|
+
> **Out-of-process Chronicle integration specs are an advanced case** — reserve them for host wiring, real infrastructure, serialization/transport, or end-to-end boundaries the scenario helpers can't exercise. They are not the default vertical-slice test shape.
|
|
28
|
+
|
|
29
|
+
**Every spec file is wrapped in `#if DEBUG … #endif`** so spec code ships only in Debug (the Debug build gate validates it and also regenerates proxies).
|
|
30
|
+
|
|
31
|
+
### `CommandScenario<TCommand>` — State Change default
|
|
32
|
+
|
|
33
|
+
Runs authorization, validators, `Provide()` (when present), and `Handle()` in-process, and exposes the appended events.
|
|
34
|
+
|
|
35
|
+
```csharp
|
|
36
|
+
#if DEBUG
|
|
37
|
+
namespace MyApp.Authors.Registration.when_registering_an_author;
|
|
38
|
+
|
|
39
|
+
public class and_all_information_is_valid : Specification
|
|
40
|
+
{
|
|
41
|
+
readonly CommandScenario<RegisterAuthor> _scenario = new();
|
|
42
|
+
readonly AuthorId _id = AuthorId.New();
|
|
43
|
+
CommandResult _result;
|
|
44
|
+
|
|
45
|
+
async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_id, "Jane Austen"));
|
|
46
|
+
|
|
47
|
+
[Fact] void should_succeed() => _result.ShouldBeSuccessful();
|
|
48
|
+
[Fact] async Task should_have_appended_registered_event() =>
|
|
49
|
+
await _scenario.ShouldHaveAppendedEvent<RegisterAuthor, AuthorRegistered>(_id, e => e.Name == "Jane Austen");
|
|
50
|
+
}
|
|
51
|
+
#endif
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `CommandScenario<TCommand>` exposes `Services` (an `IServiceCollection`), `Context`, `Execute(command)`, and `Validate(command)` — and nothing else; everything else is configured through `Services` and asserted with extension methods.
|
|
55
|
+
- **Event assertions are extension methods on the scenario** (from the Chronicle testing package), keyed by **command + event** type: `await _scenario.ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId)` or the `(eventSourceId, Func<TEvent,bool> predicate)` overload; plus `await _scenario.ShouldHaveTailSequenceNumber<TCommand>(...)`. They return `Task`, so the fact is `async Task`.
|
|
56
|
+
- **`CommandResult` assertions** (Arc extensions): `ShouldBeSuccessful()`, `ShouldNotBeSuccessful()`, `ShouldBeValid()`, `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`/`ShouldNotHaveExceptions()`.
|
|
57
|
+
- **Seed prior state through `_scenario.Services`** — there is no `Given`/`Events` on `CommandScenario`. To populate the DCB read models the validator/`Provide()`/`Handle()` inject, substitute `IReadModels` and register it (`_scenario.Services.Replace(new ServiceDescriptor(typeof(IReadModels), mock))`, mocking `GetInstanceById(...)`) or register projections with `_scenario.Services.AddReadModels(...)`.
|
|
58
|
+
- **Validator/`Provide()` dependencies:** register them in `_scenario.Services`; Arc testing discovers the concrete validator automatically. When several specs need different injected validator states, test rejected variants by instantiating the validator directly (per-scenario state can be order-sensitive under parallel xUnit).
|
|
59
|
+
- **`Provide()`:** drive it through `CommandScenario` end-to-end; when the handler's decision is pure given provided data, also test `Handle(providedValue)` directly.
|
|
60
|
+
|
|
61
|
+
#### Validation-failure assertions — non-negotiable
|
|
62
|
+
|
|
63
|
+
Every unhappy-path spec asserts **both**:
|
|
64
|
+
|
|
65
|
+
```csharp
|
|
66
|
+
[Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful();
|
|
67
|
+
[Fact] void should_have_validation_errors() => _result.ShouldHaveValidationErrors();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`ShouldNotBeSuccessful()` alone can't tell a validation rejection from an unhandled exception. **Never assert on message strings** — they're presentation text. Authorization failures use `ShouldNotBeAuthorized()` (an unauthorized result has *no* validation errors, so `ShouldHaveValidationErrors()` would silently flip). To exercise a command that carries authorization attributes, register the identity the authorization evaluator reads into `_scenario.Services` (substitute the identity provider your app uses). **Adding `[Roles]` to an existing command breaks its happy-path AND validation-failure `.Execute()` specs** — an unauthorized result is not successful and carries no validation errors; switch those assertions to `ShouldNotBeAuthorized()`.
|
|
71
|
+
|
|
72
|
+
`CommandResult` assertions (from `Cratis.Arc.Testing.Commands`; failures throw `CommandResultAssertionException`): `ShouldBeSuccessful()` (`isAuthorized && isValid && !hasExceptions`), `ShouldNotBeSuccessful()`, `ShouldBeValid()` (no validation errors — does *not* check authz/exceptions), `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`.
|
|
73
|
+
|
|
74
|
+
> **Validator-state order-sensitivity:** the command-scenario pipeline can cache enough state to make injected-validator branches order-sensitive when xUnit runs classes in parallel. Use `CommandScenario` for the valid path, test rejected state-variants by **instantiating the validator directly**, and — only for that validator-state case — put the command's specs in a **small xUnit `[Collection]`**. (This is distinct from the unique-value-collision rule below, where `[Collection]` is the *wrong* fix.)
|
|
75
|
+
|
|
76
|
+
### `EventScenario` — constraints & append semantics
|
|
77
|
+
|
|
78
|
+
```csharp
|
|
79
|
+
readonly EventScenario _scenario = new();
|
|
80
|
+
IAppendResult _result;
|
|
81
|
+
|
|
82
|
+
async Task Establish() =>
|
|
83
|
+
await _scenario.Given.ForEventSource(AuthorId.New()).Events(new AuthorRegistered("Jane Austen"));
|
|
84
|
+
|
|
85
|
+
async Task Because() =>
|
|
86
|
+
_result = await _scenario.EventLog.Append(AuthorId.New(), new AuthorRegistered("Jane Austen"));
|
|
87
|
+
|
|
88
|
+
[Fact] void should_be_failed() => _result.ShouldBeFailed();
|
|
89
|
+
[Fact] void should_violate_unique_constraint() => _result.ShouldHaveConstraintViolationFor(AuthorConstraintNames.UniqueName);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`IAppendResult` assertions (failures throw `AppendResultAssertionException`): `ShouldBeSuccessful()`, `ShouldBeFailed()`, `ShouldHaveConstraintViolations()`/`ShouldNotHave…`, `ShouldHaveConstraintViolationFor(name)`, `ShouldHaveConcurrencyViolations()`/`ShouldNotHave…`, `ShouldHaveErrors()`/`ShouldNotHave…`. Assert the constraint **name**, never the message.
|
|
93
|
+
|
|
94
|
+
### `ReadModelScenario<TReadModel>` — State View default
|
|
95
|
+
|
|
96
|
+
Drives events into the projection/reducer and asserts the resulting state. Use xUnit `Assert.*` on `_scenario.Instance`.
|
|
97
|
+
|
|
98
|
+
```csharp
|
|
99
|
+
ReadModelScenario<Author> _scenario = null!;
|
|
100
|
+
void Establish() => _scenario = new();
|
|
101
|
+
|
|
102
|
+
async Task Because() =>
|
|
103
|
+
await _scenario.Given.ForEventSource(_id).Events(new AuthorRegistered("Jane Austen"), new AuthorArchived());
|
|
104
|
+
|
|
105
|
+
[Fact] void should_be_archived() => Assert.True(_scenario.Instance!.IsArchived);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use the named-parameter constructor when services/initial state are needed: `new ReadModelScenario<Author>(initialState: null, serviceProvider: services)`. Pre-seed a keyed read model with `Given.ForEventSourceId(id).ReadModel(instance)` for code that calls `IReadModels.GetInstanceById`. Cross-stream projections (`UsingKey`/`UsingParentKey`) are supported — seed each contributing stream with its own `Given.ForEventSource(...)`.
|
|
109
|
+
|
|
110
|
+
**Bridging `IReadModels` into a `CommandScenario`** (current harness limitation): when a command handler injects `IReadModels` directly, build a focused `ReadModelScenario<T>`, seed it via `Given.ForEventSourceId(id).ReadModel(...)`, then register its read models into the command scenario — `_scenario.Services.AddSingleton(readModelScenario.ReadModels)`. Pass `scenario.ReadModels` into code under test for keyed `GetInstanceById` reads; for non-key searches/filters, register a fake lookup or an `IMongoCollection<T>` mock in the scenario services instead.
|
|
111
|
+
|
|
112
|
+
**Skip discipline:** don't pre-emptively `[Fact(Skip=...)]` a read-model assertion — assume scalar `ConceptAs<string>`, enum, and identifier properties populate correctly; if one fails, investigate the projection. Only skip on a *reproduced* harness gap, and put the specific reason in the skip message.
|
|
113
|
+
|
|
114
|
+
### `ReactorScenario<TReactor>` — Automation / Translation
|
|
115
|
+
|
|
116
|
+
Construct with an `IServiceProvider` of NSubstitute mocks; assert on the mocks after `Given` fires events. Import `Cratis.Chronicle.Testing.Reactors;`.
|
|
117
|
+
|
|
118
|
+
```csharp
|
|
119
|
+
void Establish()
|
|
120
|
+
{
|
|
121
|
+
_service = Substitute.For<IMyService>();
|
|
122
|
+
_scenario = new(new ServiceCollection().AddSingleton(_service).BuildServiceProvider());
|
|
123
|
+
}
|
|
124
|
+
async Task Because() => await _scenario.Given.ForEventSource(_id).Events(new AuthorRegistered("Jane Austen"));
|
|
125
|
+
[Fact] async Task should_notify() => await _service.Received(1).Notify("Jane Austen");
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Out-of-process Chronicle integration specs (advanced)
|
|
129
|
+
|
|
130
|
+
Reserve these for the host/transport boundary the scenario helpers can't reach. They test a complete slice — HTTP request → command → append → constraint → projection — against a real Chronicle store, and live under `when_<behavior>/` directly inside the slice folder (no `for_` folder; the "unit" is the whole slice).
|
|
131
|
+
|
|
132
|
+
```csharp
|
|
133
|
+
using context = MyApp.Authors.Registration.when_registering.and_name_already_exists.context;
|
|
134
|
+
|
|
135
|
+
namespace MyApp.Authors.Registration.when_registering;
|
|
136
|
+
|
|
137
|
+
[Collection(ChronicleCollection.Name)]
|
|
138
|
+
public class and_name_already_exists(context context) : Given<context>(context)
|
|
139
|
+
{
|
|
140
|
+
public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture)
|
|
141
|
+
{
|
|
142
|
+
public const string AuthorName = "John Doe";
|
|
143
|
+
public CommandResult<object>? Result;
|
|
144
|
+
|
|
145
|
+
async Task Establish() => await EventStore.EventLog.Append(AuthorId.New(), new AuthorRegistered(AuthorName));
|
|
146
|
+
async Task Because() => Result = await Client.ExecuteCommand<RegisterAuthor>("/api/authors/register", new RegisterAuthor(AuthorName));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
[Fact] void should_not_be_successful() => Context.Result!.IsSuccess.ShouldBeFalse();
|
|
150
|
+
[Fact] void should_have_appended_only_one_event() => Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `ExecuteCommand<TCommand>(url, cmd)` returns `CommandResult<object>?`; `ExecuteCommand<TCommand, TResult>(url, cmd)` returns `CommandResult<TResult>?`.
|
|
155
|
+
- Helpers: `Context.ShouldHaveTailSequenceNumber(n)` (First = 0), `Context.ShouldHaveAppendedEvent<TEvent>(seq, eventSourceId, validator)`.
|
|
156
|
+
|
|
157
|
+
**Async reactor follow-ups** — when a reactor fires after the command and appends further events, collect them: start the collector **before** the triggering act with `_collector = StartCollectingAppends()`, then `await _collector.WaitForCount(2, TimeSpan.FromSeconds(10))`, assert via `_collector.ShouldHaveEvent<TEvent>(e => ...)`, and `_collector.Dispose()` in `Destroy()`. (`IEventAppendCollection` from the Chronicle testing API.)
|
|
158
|
+
|
|
159
|
+
## Application spec conventions
|
|
160
|
+
|
|
161
|
+
These supplement the universal conventions in [specs.csharp.md](./specs.csharp.md#conventions):
|
|
162
|
+
|
|
163
|
+
- **Per-test values for anything in a uniqueness check** — `$"{Guid.NewGuid():N}@example.com"`, a fresh `Guid`, or a truncated id `Guid.NewGuid().ToString("N")[..9]`. Hardcoded values cause order-dependent flakes. Do not add `[Collection(...)]` to work around *collisions* (it's the wrong fix here).
|
|
164
|
+
- **Sequence numbers are zero-based** — first event = `0`, tail of two events = `1`, tail of three = `2`. Never write "tail of 1" for a single appended event; the tail of `[A]` is `0`. (A common first-pass defect.)
|
|
165
|
+
- A slice covering **multiple subjects** (several read models/constraints/aspects) groups its specs under `<Slice>/for_<Subject>/when_<behavior>/`.
|
|
166
|
+
|
|
167
|
+
## See also
|
|
168
|
+
|
|
169
|
+
- [specs.csharp.md](./specs.csharp.md) — the universal `Specification` + NSubstitute base this builds on (and what framework specs use).
|
|
170
|
+
- [vertical-slices.md](./vertical-slices.md) — what each artifact promises (the contract under spec).
|
|
171
|
+
- [efcore.specs.md](./efcore.specs.md) — `DbContext` specs with SQLite in-memory.
|
|
172
|
+
- skills: **write-specs**, **write-specs-events**, **write-specs-readmodels**.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/for_*/**/*.ts, **/when_*/**/*.ts"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/for_*/**/*.ts"
|
|
5
|
+
- "**/when_*/**/*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# How to Write TypeScript Specs
|
|
9
|
+
|
|
10
|
+
Extends the base [specs.md](./specs.md) with TypeScript-specific conventions.
|
|
11
|
+
|
|
12
|
+
> **Which TS spec guide?** For **application frontend** specs — view models, React components, helpers in a Cratis application — use [frontend-testing.md](./frontend-testing.md) (plain `describe`/`beforeEach`, view models constructible without React). This file covers the **`given()`-helper** style used inside Cratis framework TypeScript packages (`@cratis/*`). Use it when contributing to the framework packages themselves.
|
|
13
|
+
|
|
14
|
+
TypeScript specs follow the same BDD philosophy as C# specs — they describe behaviors, not implementations. The `given()` helper and context classes mirror the Cratis.Specifications pattern on the C# side: setup is separated from the action, and each `it()` assertion verifies a single outcome.
|
|
15
|
+
|
|
16
|
+
## Frameworks
|
|
17
|
+
|
|
18
|
+
- [Vitest](https://vitest.dev/) for running tests.
|
|
19
|
+
- [Mocha](https://mochajs.org) for test structure (`describe`, `it`, `beforeEach`).
|
|
20
|
+
- [SinonJS](https://sinonjs.org) for mocking/stubbing.
|
|
21
|
+
- [Chai](https://www.chaijs.com) for assertions — **always use the `.should` fluent interface**, never `expect()`. The fluent style reads as a natural sentence: `result.should.equal(expected)`.
|
|
22
|
+
- Run tests with `yarn test` from each package.
|
|
23
|
+
|
|
24
|
+
## File Structure
|
|
25
|
+
|
|
26
|
+
Tests live alongside source code in `for_`, `when_`, or `given_` folders:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
for_EventsCommandResponseValueHandler/
|
|
30
|
+
├── given/
|
|
31
|
+
│ └── an_events_command_response_value_handler.ts
|
|
32
|
+
├── when_checking_can_handle/
|
|
33
|
+
│ ├── with_valid_events_collection.ts
|
|
34
|
+
│ ├── with_null_value.ts
|
|
35
|
+
│ └── without_event_source_id.ts
|
|
36
|
+
└── when_handling/
|
|
37
|
+
├── empty_events_collection.ts
|
|
38
|
+
└── multiple_events_collection.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## BDD Pattern with `given()` Helper
|
|
42
|
+
|
|
43
|
+
The `given()` function is the TypeScript equivalent of the C# `Specification` base class. It instantiates a context class (the "given"), passes it to the test suite, and ensures setup runs before assertions. This keeps the Establish/Because/should pattern consistent across both stacks.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { an_events_command_response_value_handler } from '../given/an_events_command_response_value_handler';
|
|
47
|
+
import { given } from '../../../given';
|
|
48
|
+
|
|
49
|
+
describe('when checking can handle with valid events collection', given(an_events_command_response_value_handler, context => {
|
|
50
|
+
let result: boolean;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
result = context.handler.canHandle(context.commandContext, [new TestEvent('Test')]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return true', () => {
|
|
57
|
+
result.should.be.true;
|
|
58
|
+
});
|
|
59
|
+
}));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For behaviors with multiple outcomes, include "when \<behavior\>" as a prefix in the `describe` text.
|
|
63
|
+
|
|
64
|
+
## Reusable Context Classes
|
|
65
|
+
|
|
66
|
+
Context classes play the same role as `given/` classes in C# — they capture preconditions that multiple specs share. Unlike C# (where fields are `protected`), TypeScript context properties are public because tests access them directly through the `context` parameter.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
export class an_events_command_response_value_handler {
|
|
70
|
+
handler: EventsCommandResponseValueHandler;
|
|
71
|
+
commandContext: CommandContext;
|
|
72
|
+
|
|
73
|
+
constructor() {
|
|
74
|
+
this.commandContext = /* setup */;
|
|
75
|
+
this.handler = new EventsCommandResponseValueHandler(/* deps */);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- Properties are public (not protected) — tests access them via `context.propertyName`.
|
|
81
|
+
- Import `given` from the package root: `import { given } from '../../given';`.
|
|
82
|
+
- Simple tests without shared setup don't need a reusable context.
|
|
83
|
+
|
|
84
|
+
## Simple Test Pattern (without context)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
describe('when replacing route parameters', () => {
|
|
88
|
+
let result: { route: string; unusedParameters: object };
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
result = UrlHelpers.replaceRouteParameters('/api/items/{id}', { id: '123' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should replace the route parameter', () => {
|
|
95
|
+
result.route.should.equal('/api/items/123');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Naming Conventions
|
|
101
|
+
|
|
102
|
+
TypeScript specs use spaces in `it()` descriptions (unlike C# which uses underscores) because they appear in test runner output as human-readable sentences.
|
|
103
|
+
|
|
104
|
+
- Use **spaces** (not underscores) in `it()` descriptions:
|
|
105
|
+
- ✅ `it('should return invalid result', ...)`
|
|
106
|
+
- ❌ `it('should_return_invalid_result', ...)`
|
|
107
|
+
- Start `it()` descriptions with "should".
|
|
108
|
+
- `describe()` text describes the scenario in natural language.
|
|
109
|
+
|
|
110
|
+
## Assertions — Chai Fluent Interface
|
|
111
|
+
|
|
112
|
+
**Always use the `.should` fluent interface. Never use `expect()`.** The `.should` style reads as a natural English sentence — `value.should.equal(expected)` vs `expect(value).to.equal(expected)` — and matches the project's preference for code that reads like prose.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
value.should.equal(expected);
|
|
116
|
+
value.should.be.true;
|
|
117
|
+
value.should.be.false;
|
|
118
|
+
value.should.be.null;
|
|
119
|
+
value.should.not.be.null;
|
|
120
|
+
value.should.deep.equal(expected);
|
|
121
|
+
value.should.be.instanceOf(Type);
|
|
122
|
+
array.should.contain(item);
|
|
123
|
+
array.should.have.lengthOf(3);
|
|
124
|
+
(() => throwingFn()).should.throw(ErrorType);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Mocking with Sinon
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import sinon from 'sinon';
|
|
131
|
+
|
|
132
|
+
const stub = sinon.createStubInstance(ConcreteClass);
|
|
133
|
+
const fetchStub = sinon.stub(globalThis, 'fetch');
|
|
134
|
+
fetchStub.resolves({ ok: true, json: async () => ({ /* data */ }) });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Async
|
|
138
|
+
|
|
139
|
+
`beforeEach`, `afterEach`, and `it` callbacks can all be `async` when needed.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.stories.tsx"
|
|
3
|
+
profile: application
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*.stories.tsx"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Storybook Story Conventions
|
|
9
|
+
|
|
10
|
+
Stories are visual coverage and documentation for reusable React components built on Cratis Components — **not** the behavior test suite. Non-trivial view-model/helper behavior still needs BDD Vitest specs (see [frontend-testing.md](./frontend-testing.md)). Every story file is colocated with the component it covers.
|
|
11
|
+
|
|
12
|
+
## When to write a story
|
|
13
|
+
|
|
14
|
+
Write stories for **reusable components**: shared component-library primitives and slice-internal components reused across slices. Do **not** story live slice pages wired directly to Arc query/command hooks or a backend — exercise those in the running app. For a presentational view that needs visual coverage, extract it or render its presentational subcomponents with deterministic fixture props.
|
|
15
|
+
|
|
16
|
+
Good candidates: a presentational card/list-item/form-field with multiple visual states; a component with hover/selected/disabled/error variants; anything reusable across slices.
|
|
17
|
+
|
|
18
|
+
## Preview infrastructure (what you get for free)
|
|
19
|
+
|
|
20
|
+
A configured `.storybook/preview` should wrap every story in `CratisComponentsProvider` (from `@cratis/components/Common`) and import `@cratis/components/styles` so PrimeReact-based components render correctly. With that in place:
|
|
21
|
+
|
|
22
|
+
- **Auto prop tables** via `react-docgen-typescript` — a component's `interface` + per-prop JSDoc renders as a Docs prop table, and union props become `select` controls automatically. Keep a JSDoc comment on every prop. **`argTypes` is an override layer** (change control type, group under `table.category`, disable a control) — don't re-list every prop.
|
|
23
|
+
- **Accessibility panel** (`@storybook/addon-a11y`) scans each story with axe-core. Treat new violations as defects.
|
|
24
|
+
|
|
25
|
+
## Meta block
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
29
|
+
import { MyComponent } from './MyComponent';
|
|
30
|
+
|
|
31
|
+
const meta = {
|
|
32
|
+
title: 'Components/MyComponent', // sidebar path
|
|
33
|
+
component: MyComponent,
|
|
34
|
+
parameters: { layout: 'centered' }, // 'centered' | 'padded' | 'fullscreen'
|
|
35
|
+
tags: ['autodocs'], // always include
|
|
36
|
+
} satisfies Meta<typeof MyComponent>;
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof meta>;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use `satisfies Meta<typeof Component>` — never `: Meta<T>` (better inference). `layout`: `centered` for atoms, `padded` for compound/container components, `fullscreen` for layout shells.
|
|
43
|
+
|
|
44
|
+
## Required stories
|
|
45
|
+
|
|
46
|
+
Adapt depth to the component — rich for complex/stateful, trimmed for atoms:
|
|
47
|
+
|
|
48
|
+
1. **`Playground`** — controllable: `args` for the happy path, all props tweakable. Use `fn()` from `storybook/test` for callbacks (not `() => {}`).
|
|
49
|
+
2. **Showcase** — one or more `render:` stories covering the meaningful axes (`Variants`/`Tones`, `Sizes`, `States`, `WithIcons`). Use the Arc story-kit (`StoryContainer`, `StorySection`, `StoryGrid`, `StoryDivider`, `StoryBadge` from `@cratis/arc.react/stories`) for layout. Note: `StorySection` has no `label` prop (use an `<h3>` inside); `StoryGrid` has no `columns` prop (it auto-wraps).
|
|
50
|
+
3. **`InContext`** — the component in a small realistic composition.
|
|
51
|
+
4. **`Interactive`** — for **stateful** components: a local `const Demo: React.FC` owning `useState`, plus a `play:` test. **Omit for presentational atoms.**
|
|
52
|
+
|
|
53
|
+
Add a one-line `/** … */` doc above each story; set `parameters.docs.description.component` on the meta.
|
|
54
|
+
|
|
55
|
+
## Interaction tests (`play:`) — stateful components only
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
59
|
+
|
|
60
|
+
export const Interactive: Story = {
|
|
61
|
+
render: () => <Demo />,
|
|
62
|
+
play: async ({ canvasElement }) => {
|
|
63
|
+
const canvas = within(canvasElement);
|
|
64
|
+
await userEvent.click(canvas.getAllByRole('tab')[1]);
|
|
65
|
+
await expect(canvas.getAllByRole('tab')[1]).toHaveAttribute('aria-selected', 'true');
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **Stateful only** — presentational atoms get no `play:`. Thin PrimeReact wrappers (overlay-portal components) are excluded — their behavior is the framework's.
|
|
71
|
+
- Prefer role/structure queries over localized text. `play:` runs in the Interactions panel (documentation/QA); the enforced behavioral gate is the Vitest spec.
|
|
72
|
+
|
|
73
|
+
## Router-dependent components
|
|
74
|
+
|
|
75
|
+
Wrap with `MemoryRouter` via a story decorator when a component uses `Link`/`useNavigate`.
|
|
76
|
+
|
|
77
|
+
## Gate
|
|
78
|
+
|
|
79
|
+
`build-storybook` should be green in CI — it compiles every story and `play:` function (catching broken imports/decorators) but does not execute play assertions.
|
|
80
|
+
|
|
81
|
+
## See also
|
|
82
|
+
|
|
83
|
+
- [react.md](./react.md) — component patterns; [components.md](./components.md) — component structure/styling.
|
|
84
|
+
- [frontend-testing.md](./frontend-testing.md) — BDD Vitest specs (the behavioral gate).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*"
|
|
3
|
+
description: "Use when running any terminal command. Prefix every command with rtk -- including each command in an && chain -- so token-heavy output is filtered."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RTK (Rust Token Killer) - Token-Optimized Commands
|
|
7
|
+
|
|
8
|
+
## Golden Rule
|
|
9
|
+
|
|
10
|
+
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
|
|
11
|
+
|
|
12
|
+
**Important**: Even in command chains with `&&`, use `rtk`:
|
|
13
|
+
```bash
|
|
14
|
+
# ❌ Wrong
|
|
15
|
+
git add . && git commit -m "msg" && git push
|
|
16
|
+
|
|
17
|
+
# ✅ Correct
|
|
18
|
+
rtk git add . && rtk git commit -m "msg" && rtk git push
|
|
19
|
+
```
|