rhachet-roles-ehmpathy 1.8.0 → 1.9.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.
Files changed (45) hide show
  1. package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].v3.md +87 -0
  2. package/dist/logic/roles/mechanic/.briefs/architecture/directional-dependencies.md +49 -40
  3. package/dist/logic/roles/mechanic/.briefs/criteria.practices/require.knowledge.externalized.md +17 -0
  4. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/bad-practice/forbid.positional-args.md +43 -0
  5. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/best-practice/require.namedargs.md +6 -0
  6. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/.readme.md +0 -0
  7. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/best-practice/declastruct.[demo].md +485 -0
  8. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.nullable.md +13 -0
  9. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.undefined.md +15 -0
  10. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.refs.immuatble.md +9 -0
  11. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/best-practice/ref.package.domain-objects.[readme].md +585 -0
  12. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.operations/best-practice/require.sync.names.md +14 -0
  13. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/bad-practices/forbid.hide_errors.md +13 -0
  14. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.[demo].shell.md +17 -0
  15. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.md +28 -0
  16. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/directional-dependencies.md +82 -0
  17. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/dot-test-and-dot-temp.md +20 -0
  18. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.typescript.utils/best-practice/ref.package.as-command.[tips].md +7 -0
  19. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.acceptance/best-practice/blackbox.md +5 -0
  20. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.run.[lesson].md +18 -0
  21. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.use.[lesson].md +20 -0
  22. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].md +3 -0
  23. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_integ.md +8 -0
  24. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_units.md +9 -0
  25. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md +280 -0
  26. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/prefer.datadriven.md +41 -0
  27. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/ref.test-fns.[readme].md +185 -0
  28. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/whento.snapshots.[lesson].md +23 -0
  29. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/.readme.md +1 -0
  30. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=practices.terms=forbid_prefer_desire_require.md +13 -0
  31. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=software.terms=prodcode_vs_testcode.md +7 -0
  32. package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/.readme.md +3 -0
  33. package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.chill-nature.md +0 -0
  34. package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.lowercase.md +0 -0
  35. package/dist/logic/roles/mechanic/.skills/declapract.upgrade.sh +50 -0
  36. package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sh +113 -0
  37. package/dist/logic/roles/mechanic/.skills/link.claude.transcripts.sh +43 -0
  38. package/dist/logic/roles/mechanic/.skills/run.test.sh +245 -0
  39. package/dist/logic/roles/mechanic/.skills/test.integration.sh +50 -0
  40. package/dist/logic/roles/mechanic/getMechanicRole.js +1 -1
  41. package/dist/logic/roles/mechanic/getMechanicRole.js.map +1 -1
  42. package/package.json +3 -3
  43. package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].[idea].md +0 -35
  44. package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].md +0 -20
  45. /package/dist/logic/roles/architect/.briefs/{criteria.practices → practices}/prefer.env_access.prep_over_dev.md +0 -0
@@ -0,0 +1,82 @@
1
+ .tactic = arch:directional-deps
2
+
3
+ .what
4
+ enforce strict top-down dependency flow across layered system boundaries — lower layers must never import from higher ones
5
+
6
+ .scope
7
+ - applies to all folders and modules within `src/`
8
+ - required for `contract/`, `access/`, `domain.objects/`, `domain.operations/`
9
+ - governs imports, module references, and stitched flow boundaries
10
+
11
+ .why
12
+ - upholds **separation of concerns** and enforces **clean architecture**
13
+ - prevents circular dependencies and tangled system boundaries
14
+ - makes each layer easier to test, replace, and understand in isolation
15
+ - aligns with `arch:bounded-contexts` and enables predictable top-down orchestration
16
+
17
+ .structure
18
+ \`\`\`
19
+ src/
20
+ contract/ // topmost — public interfaces, local commands
21
+ api/ // public invocable api endpoints, deployed and exposed by the project; e.g., via `aws-lambda`
22
+ cmd/ // private internal use entrypoints, supported by the project; e.g., via `as-command`
23
+ sdk/ // public software development kit exports, supported by the project; e.g., `export ...`
24
+ cli/ // public command line interface contracts, supported by the project; e.g., via `commander`
25
+
26
+ access/ // infrastructure layer (daos, sdks, svcs)
27
+ daos/ // private persistence logic — may reference domain objects
28
+ sdks/ // remote third party contracts, from any alt org — may declare their own domain.objects
29
+ svcs/ // remote first party contracts, from our own org — may declare their own domain.objects
30
+
31
+ domain.objects/ // canonical domain declarations
32
+ domain.operations/ // domain behavior + business rules
33
+
34
+ infra/ // infrastructure specific adapters
35
+ \`\`\`
36
+
37
+ .how
38
+ - each layer may depend **only on the layers below it**
39
+ - `contract/` may depend on `domain.objects/` and `domain.operations/`
40
+ - `access/` may depend on `domain.objects/` and `domain.operations/`
41
+ - `domain.operations/` may depend on `domain.objects/` or `infra/`
42
+ - `domain.objects/` must not depend on anything outside its own layer
43
+ - `infra/` must not depend on anything outside its own layer
44
+
45
+ - stitched flows live in `domain.operations/` or `contract/commands/` and orchestrate downstream only
46
+ - never import upward across layers (e.g., `domain.objects/` importing `access/`)
47
+ - shared types must follow the same directional rules
48
+
49
+ .enforcement
50
+ - imports that violate top-down boundary = **BLOCKER**
51
+ - circular dependencies between layers = **BLOCKER**
52
+ - logic in `domain.objects/` must never reach into `access/`, `contract/`, or `domain.operations/`
53
+ - logic in `domain.operations/` must not reference infrastructure concerns; they can can only leverage `infra/` adapters
54
+ - logic in `access/` may use domain layers but must remain free of domain knowledge and business rules
55
+ - logic in `infra/` must remain free of domain knowledge and business rules
56
+
57
+ .examples
58
+
59
+ ✅ positive
60
+ \`\`\`ts
61
+ // contract/endpoints/sendInvoice.ts
62
+ import { generateInvoice } from '@/domain.operations/generateInvoice';
63
+ import { invoiceDao } from '@/access/daos/invoiceDao';
64
+
65
+ // access/daos/jobDao.ts
66
+ import { Job } from '@/domain.objects/Job';
67
+
68
+ // domain.operations/calculateTotal.ts
69
+ import { LineItem } from '@/domain.objects/LineItem';
70
+ \`\`\`
71
+
72
+ ❌ negative
73
+ \`\`\`ts
74
+ // domain.objects/Customer.ts
75
+ import { customerDao } from '@/access/daos/customerDao'; // ⛔ illegal upward import
76
+
77
+ // domain.operations/InvoiceOps.ts
78
+ import { runFlow } from '@/contract/commands'; // ⛔ direction violation
79
+
80
+ // access/svcs/sdkWrapper.ts
81
+ import { dispatchFlow } from '@/contract/'; // ⛔ bottom-up reference
82
+ \`\`\`
@@ -0,0 +1,20 @@
1
+ prefer
2
+
3
+ .temp/ directories over
4
+ - tmp/
5
+ - temp
6
+
7
+ .test/ directories over
8
+ - __test__
9
+ - __fixtures__
10
+ - __test_assets__
11
+ - __test_utils__
12
+ - etc
13
+
14
+ ---
15
+
16
+ its easier on the eyes and more consistent with broader patterns
17
+
18
+ ---
19
+
20
+ this is a NITPICK level violation
@@ -0,0 +1,7 @@
1
+ when using `@ehmpathy/as-command`, no need to manage output dirs yourslef
2
+
3
+ simply use context.out.write()
4
+
5
+ it will write to a dedicated, safe output dir
6
+
7
+ ---
@@ -0,0 +1,5 @@
1
+
2
+ acceptance tests are black box tests
3
+
4
+ - no ability to assert internal details
5
+ - only what's exposed through the @src/contract layer is accessible to acpt tests
@@ -0,0 +1,18 @@
1
+ 1. lookup the organization to use from declapract.use.yml
2
+ 2. declare the AWS_PROFILE based on the pattern `AWS_PROFILE=$organization.dev`
3
+ 1. always use .dev, never .prod
4
+ 2. this specifies which remote resources we'll have access to for the session
5
+ 3. we always test against dev resources, to avoid prod pollution
6
+ 3. run npm run start:testdb (or provision:integration-test-db, if start:testdb is not available yet)
7
+ 1. this setsup the local testdb against which the tests can be run
8
+
9
+ then, you can run the tests you need to
10
+
11
+ e.g.,
12
+
13
+ AWS_PROFILE=$organization.dev npm run test:integration -- syncPhoneFromWhodis.integration.test.ts
14
+ AWS_PROFILE=$organization.dev npm run test:unit -- syncPhoneFromWhodis.test.ts
15
+
16
+ etc
17
+
18
+ check package json for the other test variants you can run
@@ -0,0 +1,20 @@
1
+ instead of rerunning tests over and over and getting the `head` to check what happened while preserving context
2
+
3
+ its BEST
4
+
5
+ to `| tee` into a `@gitroot/.log/test/(unit|integration|acceptance)/run.$ISOTIMESTAMP.out` file
6
+
7
+ then you can review that file over and over
8
+
9
+ also, other agents can review as well in parallel
10
+
11
+ last, it can also be used to compare progress in changes of tests
12
+
13
+ ---
14
+
15
+ so, pretty much ALWAYS, you should ` | tee` into one of these files
16
+
17
+
18
+ ===
19
+
20
+ best practice is to run test via `.skills/run.test.sh`, which does this for you
@@ -0,0 +1,3 @@
1
+ import { given, when, then, useBeforeAll } from 'test-fns';
2
+
3
+ use those to declare the tests
@@ -0,0 +1,8 @@
1
+
2
+ to create an integration test, just add a file with .integration.test.ts extension (collocated) and dont mock anything
3
+
4
+ you can spy, but never mock
5
+
6
+ ---
7
+
8
+ use the less on in src/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md for preference on pattern
@@ -0,0 +1,9 @@
1
+
2
+ for unit tests:
3
+
4
+ only test that the behaviors explicitly important for that test
5
+
6
+ no need to overtest, only whatever is scoped for that test and unique to it
7
+
8
+
9
+ use the less on in src/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md for preference on pattern
@@ -0,0 +1,280 @@
1
+ # How to Write BDD Style Tests
2
+
3
+ This guide explains the pattern for writing integration tests using `test-fns` with `given`, `when`, `then`, and `useBeforeAll`.
4
+
5
+ ## Core Pattern
6
+
7
+ ```typescript
8
+ import { given, when, then, useBeforeAll } from 'test-fns';
9
+
10
+ describe('featureName', () => {
11
+ // Shared setup for all tests in the describe block
12
+ const dbConnection = useBeforeAll(() => getDatabaseConnection());
13
+ afterAll(async () => dbConnection.end());
14
+
15
+ given('[case1] description of the initial state', () => {
16
+ // Setup specific to this case, shared across all when/then blocks
17
+ const scene = useBeforeAll(async () => {
18
+ // Create test data
19
+ const entity = await createEntity({ dbConnection });
20
+ return { entity };
21
+ });
22
+
23
+ when('[t0] action or event occurs', () => {
24
+ then('expected outcome', async () => {
25
+ // Execute and verify using scene.entity
26
+ const result = await performAction({ id: scene.entity.id });
27
+ expect(result).toEqual(expectedValue);
28
+ });
29
+ });
30
+
31
+ when('[t1] different action occurs', () => {
32
+ then('different expected outcome', async () => {
33
+ // Another test using the same scene
34
+ const result = await performOtherAction({ id: scene.entity.id });
35
+ expect(result).toEqual(otherExpectedValue);
36
+ });
37
+ });
38
+ });
39
+ });
40
+ ```
41
+
42
+ ## Key Principles
43
+
44
+ ### 1. Wrap Everything in `describe`
45
+
46
+ All tests for a feature should be wrapped in a single `describe` block:
47
+
48
+ ```typescript
49
+ describe('syncPhoneFromWhodis', () => {
50
+ // all given/when/then blocks go here
51
+ });
52
+ ```
53
+
54
+ ### 2. Use `useBeforeAll` for shared resources
55
+
56
+ Instead of `let` + `beforeAll` + `afterAll`:
57
+
58
+ e.g.,
59
+ ```typescript
60
+ // ❌ Don't do this
61
+ let dbConnection: DatabaseConnection;
62
+ beforeAll(async () => {
63
+ dbConnection = await getDatabaseConnection();
64
+ });
65
+ afterAll(async () => {
66
+ await dbConnection.end();
67
+ });
68
+
69
+ // ✅ Do this
70
+ const dbConnection = useBeforeAll(() => getDatabaseConnection());
71
+ afterAll(async () => dbConnection.end());
72
+ ```
73
+
74
+ ### 3. Label given(scenes) with `[caseN]`
75
+
76
+ Each `given` block should have a unique case label:
77
+
78
+ ```typescript
79
+ given('[case1] doer with outdated phone', () => { ... });
80
+ given('[case2] doer with matching phone', () => { ... });
81
+ given('[case3] doer does not exist', () => { ... });
82
+ ```
83
+
84
+ ### 4. Label when(event) with `[tN]`
85
+
86
+ Each `when` block should have an event time index label. The counter resets within each `given` block:
87
+
88
+ ```typescript
89
+ given('[case1] first scenario', () => {
90
+ when('[t0] command executed in PLAN mode', () => { ... });
91
+ when('[t1] command executed in EXECUTE mode', () => { ... });
92
+ });
93
+
94
+ given('[case2] second scenario', () => {
95
+ when('[t0] first action', () => { ... }); // counter resets to 0
96
+ when('[t1] second action', () => { ... });
97
+ });
98
+ ```
99
+
100
+ ### 5. One Behavioral Assertion per `then` Block
101
+
102
+ Each `then` block should test a single behavioral assertion. This makes test failures more precise and test names more descriptive:
103
+
104
+ ```typescript
105
+ // ❌ Don't do this - multiple assertions in one then
106
+ when('[t0] command executed in PLAN mode', () => {
107
+ then('decision is UPDATE and doer remains unchanged', async () => {
108
+ const result = await command({ mode: 'PLAN' });
109
+ expect(result.decision).toEqual('UPDATE');
110
+ expect(result.before.doer.contactPhoneNumber).toEqual('+13175550200');
111
+
112
+ const doerAfter = await doerDao.findByUnique({ dbConnection, userUuid });
113
+ expect(doerAfter?.contactPhoneNumber).toEqual('+13175550200');
114
+ });
115
+ });
116
+
117
+ // ✅ Do this - separate then blocks for each behavioral assertion
118
+ when('[t0] command executed in PLAN mode', () => {
119
+ then('decision is "UPDATE"', async () => {
120
+ const result = await command({ mode: 'PLAN' });
121
+ expect(result.decision).toEqual('UPDATE');
122
+ });
123
+
124
+ then('before.doer.contactPhoneNumber is "+13175550200"', async () => {
125
+ const result = await command({ mode: 'PLAN' });
126
+ expect(result.before.doer.contactPhoneNumber).toEqual('+13175550200');
127
+ });
128
+
129
+ then('doer contactPhoneNumber remains unchanged', async () => {
130
+ await command({ mode: 'PLAN' });
131
+ const doerAfter = await doerDao.findByUnique({ dbConnection, userUuid });
132
+ expect(doerAfter?.contactPhoneNumber).toEqual('+13175550200');
133
+ });
134
+ });
135
+ ```
136
+
137
+ ### 6. Use `scene` for Shared Test Data
138
+
139
+ When multiple `when/then` blocks need the same test data, use `useBeforeAll` to create a `scene`:
140
+
141
+ ```typescript
142
+ given('[case1] description', () => {
143
+ const scene = useBeforeAll(async () => {
144
+ const doer = await createDoer({ dbConnection });
145
+ const provider = await createProvider({ dbConnection, doerId: doer.id });
146
+ return { doer, provider };
147
+ });
148
+
149
+ when('[t0] first test', () => {
150
+ then('outcome', async () => {
151
+ // Access scene.doer and scene.provider
152
+ const result = await action({ doerId: scene.doer.id });
153
+ });
154
+ });
155
+
156
+ when('[t1] second test', () => {
157
+ then('outcome', async () => {
158
+ // Same scene is reused
159
+ const result = await otherAction({ providerId: scene.provider.id });
160
+ });
161
+ });
162
+ });
163
+ ```
164
+
165
+ ### 7. Cases Without Setup
166
+
167
+ If a case doesn't need setup (e.g., testing error handling with invalid input), skip the `scene`:
168
+
169
+ ```typescript
170
+ given('[case4] valid userUuid', () => {
171
+ when('[t7] Whodis user cannot be found', () => {
172
+ then('command throws error', async () => {
173
+ await expect(
174
+ command({ userUuid: '00000000-0000-0000-0000-000000000001' }),
175
+ ).rejects.toThrow('Whodis user not found');
176
+ });
177
+ });
178
+ });
179
+ ```
180
+
181
+ ## Complete Example
182
+
183
+ ```typescript
184
+ import { given, when, then, useBeforeAll } from 'test-fns';
185
+ import { getDatabaseConnection } from '../../utils/database/getDatabaseConnection';
186
+ import { myCommand } from './myCommand';
187
+
188
+ describe('myCommand', () => {
189
+ const dbConnection = useBeforeAll(() => getDatabaseConnection());
190
+ afterAll(async () => dbConnection.end());
191
+
192
+ given('[case1] entity exists with state A', () => {
193
+ const scene = useBeforeAll(async () => {
194
+ const entity = await createEntity({
195
+ dbConnection,
196
+ state: 'A',
197
+ });
198
+ return { entity };
199
+ });
200
+
201
+ when('[t0] command executed in PLAN mode', () => {
202
+ then('decision is "UPDATE"', async () => {
203
+ const result = await myCommand({
204
+ entityId: scene.entity.id,
205
+ mode: 'PLAN',
206
+ });
207
+ expect(result.decision).toEqual('UPDATE');
208
+ });
209
+
210
+ then('entity state remains "A"', async () => {
211
+ await myCommand({ entityId: scene.entity.id, mode: 'PLAN' });
212
+ const entityAfter = await findEntity({ dbConnection, id: scene.entity.id });
213
+ expect(entityAfter.state).toEqual('A');
214
+ });
215
+ });
216
+
217
+ when('[t1] command executed in EXECUTE mode', () => {
218
+ then('decision is "UPDATE"', async () => {
219
+ const result = await myCommand({
220
+ entityId: scene.entity.id,
221
+ mode: 'EXECUTE',
222
+ });
223
+ expect(result.decision).toEqual('UPDATE');
224
+ });
225
+
226
+ then('after.state is "B"', async () => {
227
+ const result = await myCommand({
228
+ entityId: scene.entity.id,
229
+ mode: 'EXECUTE',
230
+ });
231
+ expect(result.after.state).toEqual('B');
232
+ });
233
+
234
+ then('entity state is updated to "B"', async () => {
235
+ await myCommand({ entityId: scene.entity.id, mode: 'EXECUTE' });
236
+ const entityAfter = await findEntity({ dbConnection, id: scene.entity.id });
237
+ expect(entityAfter.state).toEqual('B');
238
+ });
239
+ });
240
+ });
241
+
242
+ given('[case2] entity already in state B', () => {
243
+ const scene = useBeforeAll(async () => {
244
+ const entity = await createEntity({
245
+ dbConnection,
246
+ state: 'B',
247
+ });
248
+ return { entity };
249
+ });
250
+
251
+ when('[t0] command executed', () => {
252
+ then('decision is "NOCHANGE"', async () => {
253
+ const result = await myCommand({
254
+ entityId: scene.entity.id,
255
+ mode: 'EXECUTE',
256
+ });
257
+ expect(result.decision).toEqual('NOCHANGE');
258
+ });
259
+ });
260
+ });
261
+
262
+ given('[case3] invalid entityId', () => {
263
+ when('[t0] command executed', () => {
264
+ then('throws error', async () => {
265
+ await expect(
266
+ myCommand({ entityId: 'invalid-id', mode: 'PLAN' }),
267
+ ).rejects.toThrow('Entity not found');
268
+ });
269
+ });
270
+ });
271
+ });
272
+ ```
273
+
274
+ ## Benefits
275
+
276
+ 1. **Readable test output**: Test names clearly show the scenario being tested
277
+ 2. **Efficient setup**: `useBeforeAll` runs once per `given` block, not per test
278
+ 3. **Immutable references**: `const scene` and `const dbConnection` prevent accidental reassignment
279
+ 4. **Clear labeling**: `[caseN]` and `[tN]` labels make it easy to identify and discuss specific tests
280
+ 5. **Black-box testing**: Tests interact only through the contract layer, not internal implementations
@@ -0,0 +1,41 @@
1
+ when possible, prefer data driven, caselist based, tests
2
+
3
+ this is especially applicable for unit tests, which often evaluate a transform
4
+
5
+ ---
6
+
7
+
8
+ for example
9
+
10
+
11
+ ```ts
12
+
13
+ const TEST_CASES = [
14
+ {
15
+ description: 'capitalizes the first word in a sentence',
16
+ given: {
17
+ input: 'the bird is in the basket',
18
+ },
19
+ expect: {
20
+ output: 'The bird is in the basket',
21
+ }
22
+ },
23
+ {
24
+ description: 'retains existing capitals in the sentence',
25
+ given: {
26
+ input: 'that Doctor Goose is a loon!',
27
+ },
28
+ expect: {
29
+ output: 'That Doctor Goose is a loon!',
30
+ }
31
+ },
32
+ ]
33
+
34
+ describe('asSentenceCase', () => {
35
+ TEST_CASES.map(thisCase => test(thisCase.description, () => {
36
+ const output = asSentenceCase(thisCase.given.input);
37
+ expect(output).toEqual(thisCase.expect.output);
38
+ }))
39
+ })
40
+
41
+ ```
@@ -0,0 +1,185 @@
1
+ # test-fns
2
+
3
+ ![ci_on_commit](https://github.com/ehmpathy/test-fns/workflows/ci_on_commit/badge.svg)
4
+ ![deploy_on_tag](https://github.com/ehmpathy/test-fns/workflows/deploy_on_tag/badge.svg)
5
+
6
+ write usecase driven tests systematically for simpler, safer, and more readable code
7
+
8
+ # purpose
9
+
10
+ establishes a pattern of writing tests for simpler, safer, and more readable code.
11
+
12
+ by defining tests in terms of usecases (`given`, `when`, `then`) your tests are
13
+ - simpler to write
14
+ - easier to read
15
+ - safer to trust
16
+
17
+ # install
18
+
19
+ ```sh
20
+ npm install --save test-fns
21
+ ```
22
+
23
+ # use
24
+
25
+ ```ts
26
+ type Plant = { id: number, hydration: 'DRY' | 'WET' };
27
+ const doesPlantNeedWater = (plant: Plant) => plant.hydration === 'DRY';
28
+
29
+ describe('doesPlantNeedWater', () => {
30
+ given('a plant', () => {
31
+ when('the plant doesnt have enough water', () => {
32
+ const plant: Plant = {
33
+ id: 7,
34
+ hydration: 'DRY',
35
+ };
36
+ then('it should return true', () => {
37
+ expect(doesPlantNeedWater(plant)).toEqual(true)
38
+ })
39
+ })
40
+ })
41
+ })
42
+ ```
43
+
44
+ produces
45
+
46
+ ```sh
47
+ PASS src/givenWhenThen.test.ts
48
+ doesPlantNeedWater
49
+ given: a plant
50
+ when: the plant doesnt have enough water
51
+ ✓ then: it should return true (1 ms)
52
+ ```
53
+
54
+ # features
55
+
56
+
57
+ ### .runIf(condition) && .skipIf(condition)
58
+
59
+ skip running the suite if the condition is not met
60
+
61
+ ```ts
62
+ describe('your test', () => {
63
+ given.runIf(onLocalMachine)('some test that should only run locally', () => {
64
+ then.skipIf(onProduction)('some test that should not run against production', () => {
65
+ expect(onProduction).toBeFalse()
66
+ })
67
+ })
68
+ })
69
+ ```
70
+
71
+ ### usePrep
72
+
73
+ prepare test scenarios within a .given/.when block asynchronously, without any `let`s or `beforeAll`s
74
+
75
+ `usePrep` accepts a `mode` option to control when setup runs:
76
+ - `mode: 'beforeAll'` - runs setup once for all tests (default)
77
+ - `mode: 'beforeEach'` - runs setup fresh before each test
78
+
79
+ ```ts
80
+ given('an overdue invoice', () => {
81
+ const invoice = usePrep(async () => {
82
+ const invoiceOverdue = await ... // your logic
83
+ return invoiceOverdue;
84
+ })
85
+
86
+ then('it should invoke a reminder', async () => {
87
+ const result = await nurtureInvoice({ invoice }, context)
88
+ expect(result.sent.reminder).toEqual(true)
89
+ })
90
+ })
91
+ ```
92
+
93
+ **useBeforeAll and useBeforeEach are convenience wrappers** around `usePrep`:
94
+ - `useBeforeAll(setup)` is equivalent to `usePrep(setup, { mode: 'beforeAll' })`
95
+ - `useBeforeEach(setup)` is equivalent to `usePrep(setup, { mode: 'beforeEach' })`
96
+
97
+ Use the named functions for clarity about when setup runs.
98
+
99
+ ### useBeforeAll
100
+
101
+ prepare test resources once for all tests in a suite, optimizing setup time for expensive operations
102
+
103
+ ```ts
104
+ describe('spaceship refueling system', () => {
105
+ given('a spaceship that needs to refuel', () => {
106
+ const spaceship = useBeforeAll(async () => {
107
+ // This runs once before all tests in this suite
108
+ const ship = await prepareExampleSpaceship();
109
+ await ship.dock();
110
+ return ship;
111
+ });
112
+
113
+ when('no changes are made', () => {
114
+ then('it should be docked', async () => {
115
+ expect(spaceship.isDocked).toEqual(true);
116
+ });
117
+
118
+ then('it should need fuel', async () => {
119
+ expect(spaceship.fuelLevel).toBeLessThan(spaceship.fuelCapacity);
120
+ });
121
+ });
122
+
123
+ when('it connects to the fuel station', () => {
124
+ const result = useBeforeAll(async () => await spaceship.connectToFuelStation());
125
+
126
+ then('it should be connected', async () => {
127
+ expect(result.connected).toEqual(true);
128
+ });
129
+
130
+ then('it should calculate required fuel', async () => {
131
+ expect(result.fuelNeeded).toBeGreaterThan(0);
132
+ });
133
+ });
134
+ });
135
+ });
136
+ ```
137
+
138
+ ### useBeforeEach
139
+
140
+ prepare fresh test resources before each test, ensuring test isolation
141
+
142
+ ```ts
143
+ describe('spaceship combat system', () => {
144
+ given('a spaceship in battle', () => {
145
+ // This runs before each test, ensuring a fresh spaceship
146
+ const spaceship = useBeforeEach(async () => {
147
+ const ship = await prepareExampleSpaceship();
148
+ await ship.resetShields();
149
+ return ship;
150
+ });
151
+
152
+ when('no changes are made', () => {
153
+ then('it should have full shields', async () => {
154
+ expect(spaceship.shields).toEqual(100);
155
+ });
156
+
157
+ then('it should be ready for combat', async () => {
158
+ expect(spaceship.status).toEqual('READY');
159
+ });
160
+ });
161
+
162
+ when('it takes damage', () => {
163
+ const result = useBeforeEach(async () => await spaceship.takeDamage(25));
164
+
165
+ then('it should reduce shield strength', async () => {
166
+ expect(spaceship.shields).toEqual(75);
167
+ });
168
+
169
+ then('it should return damage report', async () => {
170
+ expect(result.damageReceived).toEqual(25);
171
+ });
172
+ });
173
+ });
174
+ });
175
+ ```
176
+
177
+ **When to use each:**
178
+ - `useBeforeAll`: Use when setup is expensive (database connections, API calls) and tests don't modify the resource
179
+ - `useBeforeEach`: Use when tests modify the resource and need isolation between runs
180
+ - `usePrep`: The base function that powers both - use when you want explicit control over the mode or need to dynamically choose between `beforeAll` and `beforeEach`
181
+
182
+ **Key differences:**
183
+ - All three functions (`usePrep`, `useBeforeAll`, `useBeforeEach`) create a proxy that defers setup until the test framework's lifecycle hooks run
184
+ - `useBeforeAll` and `useBeforeEach` are just clearer, more readable shortcuts for `usePrep` with a specific mode
185
+ - Choose based on readability: use `useBeforeAll`/`useBeforeEach` for explicit intent, or `usePrep` when mode needs to be configurable