rhachet-roles-ehmpathy 1.17.20 → 1.17.22
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.
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# rule.forbid.remote-boundaries
|
|
2
|
+
|
|
3
|
+
## .what
|
|
4
|
+
|
|
5
|
+
unit tests must not cross remote boundaries — if a test touches external resources, it is an integration test, not a unit test
|
|
6
|
+
|
|
7
|
+
## .why
|
|
8
|
+
|
|
9
|
+
- **unit tests verify atomic logic** — pure functions with injected dependencies
|
|
10
|
+
- **remote boundaries introduce flake** — network timeouts, file locks, db state
|
|
11
|
+
- **remote boundaries introduce latency** — disk i/o, connection overhead, query time
|
|
12
|
+
- **remote boundaries hide in mocks** — mocks create false confidence; the mock behaves differently than reality
|
|
13
|
+
- **test classification matters** — `npm run test:unit` must be fast, deterministic, and parallelizable
|
|
14
|
+
|
|
15
|
+
## .scope
|
|
16
|
+
|
|
17
|
+
applies to all files with `.test.ts` extension (unit tests)
|
|
18
|
+
|
|
19
|
+
does NOT apply to `.integration.test.ts` or `.acceptance.test.ts`
|
|
20
|
+
|
|
21
|
+
## .remote boundaries
|
|
22
|
+
|
|
23
|
+
a remote boundary is any resource external to the process memory:
|
|
24
|
+
|
|
25
|
+
| boundary | examples | why it's remote |
|
|
26
|
+
| ---------- | ------------------------------------------------------------- | -------------------------------------------------- |
|
|
27
|
+
| filesystem | `fs.readFile`, `fs.writeFile`, `path.resolve` with real paths | disk i/o, file locks, path differences across os |
|
|
28
|
+
| database | `pg.query`, `mysql.execute`, dao calls | connection state, query latency, data dependencies |
|
|
29
|
+
| network | `fetch`, `axios`, http clients, sdk calls | latency, availability, rate limits |
|
|
30
|
+
|
|
31
|
+
## .how
|
|
32
|
+
|
|
33
|
+
### 👍 unit test — pure logic leaf
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// computeInvoiceTotal.test.ts
|
|
37
|
+
describe('computeInvoiceTotal', () => {
|
|
38
|
+
it('sums line items correctly', () => {
|
|
39
|
+
const result = computeInvoiceTotal({
|
|
40
|
+
lineItems: [
|
|
41
|
+
{ amount: 100 },
|
|
42
|
+
{ amount: 50 },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
expect(result).toEqual(150);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 👍 unit test — dependency injection enables isolation
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// sendInvoice.test.ts
|
|
54
|
+
describe('sendInvoice', () => {
|
|
55
|
+
it('returns sent status when email succeeds', async () => {
|
|
56
|
+
// inject a fake, not a mock of a real service
|
|
57
|
+
const svcEmailDemo = {
|
|
58
|
+
send: async () => ({ success: true }),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const result = await sendInvoice(
|
|
62
|
+
{ invoice: exampleInvoice }, // input stays pure as always
|
|
63
|
+
{ svcEmail: svcEmailDemo }, // context has remote interfaces injected; makes it clear that _any_ shape that fits this contract is allowed
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result.sent).toEqual(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 👎 bad — crosses filesystem boundary
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// ❌ this is an integration test, not a unit test
|
|
75
|
+
describe('loadConfig', () => {
|
|
76
|
+
it('reads config from disk', async () => {
|
|
77
|
+
const config = await loadConfig({ path: './config.json' }); // fs access
|
|
78
|
+
expect(config.apiKey).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 👎 bad — crosses database boundary
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// ❌ this is an integration test, not a unit test
|
|
87
|
+
describe('getUserById', () => {
|
|
88
|
+
it('fetches user from database', async () => {
|
|
89
|
+
const user = await getUserById({ id: '123' }, { dbConnection }); // db access
|
|
90
|
+
expect(user.name).toEqual('alice');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 👎 bad — mocks hide the boundary (antipattern)
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// ❌ mocks are an antipattern — they create false confidence
|
|
99
|
+
jest.mock('fs');
|
|
100
|
+
const fs = require('fs');
|
|
101
|
+
|
|
102
|
+
describe('loadConfig', () => {
|
|
103
|
+
it('reads config', async () => {
|
|
104
|
+
fs.readFile.mockResolvedValue('{"key": "value"}');
|
|
105
|
+
const config = await loadConfig({ path: './config.json' });
|
|
106
|
+
expect(config.key).toEqual('value');
|
|
107
|
+
// 👎 this test passes but real fs.readFile behaves differently
|
|
108
|
+
// 👎 the mock doesn't validate encode, error handler, path resolution
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## .what to do instead
|
|
114
|
+
|
|
115
|
+
| if your test needs... | then... |
|
|
116
|
+
| --------------------- | ------------------------------ |
|
|
117
|
+
| filesystem access | move to `.integration.test.ts` |
|
|
118
|
+
| database queries | move to `.integration.test.ts` |
|
|
119
|
+
| network calls | move to `.integration.test.ts` |
|
|
120
|
+
|
|
121
|
+
## .the mock antipattern
|
|
122
|
+
|
|
123
|
+
mocks are forbidden because:
|
|
124
|
+
|
|
125
|
+
1. **mocks lie** — they return what you tell them, not what the real dependency returns
|
|
126
|
+
2. **mocks drift** — when the real dependency changes, the mock doesn't
|
|
127
|
+
3. **mocks hide bugs** — the test passes but production fails
|
|
128
|
+
4. **mocks test the mock** — you verify your mock setup, not your logic
|
|
129
|
+
|
|
130
|
+
### instead of mocks, use:
|
|
131
|
+
|
|
132
|
+
- **fakes** — simplified implementations that behave like the real dependency
|
|
133
|
+
- **dependency injection** — pass the dependency as a parameter
|
|
134
|
+
- **integration tests** — test the real dependency in `.integration.test.ts`
|
|
135
|
+
|
|
136
|
+
## .classification guide
|
|
137
|
+
|
|
138
|
+
| test characteristics | classification | file extension |
|
|
139
|
+
| -------------------------------------- | -------------- | ---------------------- |
|
|
140
|
+
| pure logic, no i/o, injected deps | unit | `.test.ts` |
|
|
141
|
+
| touches db, fs, network | integration | `.integration.test.ts` |
|
|
142
|
+
| blackbox via built artifact's contract | acceptance | `.acceptance.test.ts` |
|
|
143
|
+
|
|
144
|
+
## .enforcement
|
|
145
|
+
|
|
146
|
+
unit test that crosses remote boundary = **BLOCKER**
|
|
147
|
+
|
|
148
|
+
mock usage in unit tests = **BLOCKER**
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
> unit tests are fast, deterministic, and test logic in isolation.
|
|
153
|
+
> if it touches the outside world, it's not a unit test.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
### .rule = forbid-buzzwords
|
|
2
|
+
|
|
3
|
+
#### .what
|
|
4
|
+
buzzwords must be avoided — they obscure intent
|
|
5
|
+
|
|
6
|
+
#### .scope
|
|
7
|
+
- code: variable names, function names, type names, comments
|
|
8
|
+
- docs: markdown, briefs, prompts, commit messages
|
|
9
|
+
- comms: pr descriptions, reviews, discussions
|
|
10
|
+
|
|
11
|
+
#### .why
|
|
12
|
+
buzzwords have **large gravity** — they pull the mind toward them
|
|
13
|
+
|
|
14
|
+
this gravity causes **semantic diffusion**:
|
|
15
|
+
- the word feels meaningful, so thought stops
|
|
16
|
+
- precision erodes because the term is overloaded
|
|
17
|
+
- ambiguity creeps in because everyone maps it to their own interpretation
|
|
18
|
+
- communication degrades because we think we agree when we don't
|
|
19
|
+
|
|
20
|
+
it's a smell. flies buzz around smells. we don't like smells -> we dont like buzz.
|
|
21
|
+
|
|
22
|
+
#### .instead
|
|
23
|
+
think from the core. build the concept from the ground up.
|
|
24
|
+
|
|
25
|
+
find the most appropriate non-buzz word:
|
|
26
|
+
- what specifically do you mean?
|
|
27
|
+
- what is the precise action, object, or attribute?
|
|
28
|
+
- what word captures exactly this, with no surplus baggage?
|
|
29
|
+
|
|
30
|
+
precision > familiarity. clarity > trendiness.
|
|
31
|
+
|
|
32
|
+
#### .enforcement
|
|
33
|
+
buzzwords = **BLOCKER** on first encounter. if retry with justification, allow.
|
|
34
|
+
|
|
35
|
+
#### .the test
|
|
36
|
+
if you remove the buzzword and the sentence still makes sense — you didn't need it.
|
|
37
|
+
if you replace it with a concrete term and meaning is preserved — use the concrete term.
|
|
38
|
+
if you can't replace it with anything concrete — you don't know what you mean yet.
|
|
39
|
+
|
|
40
|
+
#### .note: context matters
|
|
41
|
+
some terms are buzzwords in one context but precise in another:
|
|
42
|
+
- `scalable` in marketing = buzz
|
|
43
|
+
- `scalable` in distributed systems with defined semantics = maybe ok (but still prefer specifics)
|
|
44
|
+
|
|
45
|
+
when in doubt, be more specific.
|
|
46
|
+
|
|
47
|
+
#### .see also
|
|
48
|
+
- `rule.prefer.lowercase` — no shouts
|
|
49
|
+
- `rule.require.ubiqlang` — consistent terminology
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "rhachet-roles-ehmpathy",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "empathetic software construction roles and skills, via rhachet",
|
|
5
|
-
"version": "1.17.
|
|
5
|
+
"version": "1.17.22",
|
|
6
6
|
"repository": "ehmpathy/rhachet-roles-ehmpathy",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
|
|
8
8
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
53
53
|
"prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi",
|
|
54
54
|
"test:format:biome": "biome format",
|
|
55
|
-
"prepare:rhachet": "rhachet init
|
|
55
|
+
"prepare:rhachet": "rhachet init --roles behaver mechanic reviewer"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@ehmpathy/as-command": "1.0.3",
|
|
@@ -92,10 +92,10 @@
|
|
|
92
92
|
"esbuild-register": "3.6.0",
|
|
93
93
|
"husky": "8.0.3",
|
|
94
94
|
"jest": "30.2.0",
|
|
95
|
-
"rhachet": "1.22.
|
|
96
|
-
"rhachet-roles-bhrain": "0.5.
|
|
97
|
-
"rhachet-roles-bhuild": "0.6.
|
|
98
|
-
"rhachet-roles-ehmpathy": "
|
|
95
|
+
"rhachet": "1.22.7",
|
|
96
|
+
"rhachet-roles-bhrain": "0.5.11",
|
|
97
|
+
"rhachet-roles-bhuild": "0.6.6",
|
|
98
|
+
"rhachet-roles-ehmpathy": "1.17.20",
|
|
99
99
|
"test-fns": "1.6.0",
|
|
100
100
|
"tsc-alias": "1.8.10",
|
|
101
101
|
"tsx": "4.20.6",
|