rhachet-roles-ehmpathy 1.17.19 → 1.17.21

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.
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.19",
5
+ "version": "1.17.21",
6
6
  "repository": "ehmpathy/rhachet-roles-ehmpathy",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
8
8
  "keywords": [
@@ -16,7 +16,8 @@
16
16
  "node": ">=8.0.0"
17
17
  },
18
18
  "files": [
19
- "/dist"
19
+ "/dist",
20
+ "rhachet.repo.yml"
20
21
  ],
21
22
  "scripts": {
22
23
  "build:ts": "tsc -p ./tsconfig.build.json",
@@ -27,7 +28,9 @@
27
28
  "fix": "npm run fix:format && npm run fix:lint",
28
29
  "build:clean": "chmod -R u+w dist 2>/dev/null; rm -rf dist/",
29
30
  "build:compile": "tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
30
- "build:complete": "rsync -a --prune-empty-dirs --include='*/' --exclude='**/.route/**' --exclude='**/.scratch/**' --exclude='**/.behavior/**' --exclude='**/*.test.sh' --include='**/readme.md' --include='**/*.template.md' --include='**/briefs/**/*.md' --include='**/briefs/*.md' --include='**/skills/**/*.sh' --include='**/skills/*.sh' --include='**/skills/**/*.jsonc' --include='**/skills/*.jsonc' --include='**/inits/**/*.sh' --include='**/inits/*.sh' --include='**/inits/**/*.jsonc' --include='**/inits/*.jsonc' --exclude='*' src/ dist/",
31
+ "build:complete:dist": "rsync -a --prune-empty-dirs --include='*/' --exclude='**/.route/**' --exclude='**/.scratch/**' --exclude='**/.behavior/**' --exclude='**/*.test.sh' --include='**/readme.md' --include='**/*.template.md' --include='**/briefs/**/*.md' --include='**/briefs/*.md' --include='**/skills/**/*.sh' --include='**/skills/*.sh' --include='**/skills/**/*.jsonc' --include='**/skills/*.jsonc' --include='**/inits/**/*.sh' --include='**/inits/*.sh' --include='**/inits/**/*.jsonc' --include='**/inits/*.jsonc' --exclude='*' src/ dist/",
32
+ "build:complete:repo": "npx rhachet repo introspect",
33
+ "build:complete": "npm run build:complete:dist && npm run build:complete:repo",
31
34
  "build": "npm run build:clean && npm run build:compile && npm run build:complete --if-present",
32
35
  "test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
33
36
  "test:types": "tsc -p ./tsconfig.json --noEmit",
@@ -89,10 +92,10 @@
89
92
  "esbuild-register": "3.6.0",
90
93
  "husky": "8.0.3",
91
94
  "jest": "30.2.0",
92
- "rhachet": "1.21.5",
93
- "rhachet-roles-bhrain": "0.5.9",
94
- "rhachet-roles-bhuild": "0.6.4",
95
- "rhachet-roles-ehmpathy": "link:.",
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",
96
99
  "test-fns": "1.6.0",
97
100
  "tsc-alias": "1.8.10",
98
101
  "tsx": "4.20.6",
@@ -0,0 +1,19 @@
1
+ slug: ehmpathy
2
+ readme: dist/domain.roles/readme.md
3
+ roles:
4
+ - slug: architect
5
+ readme: dist/domain.roles/architect/readme.md
6
+ briefs:
7
+ dirs: dist/domain.roles/architect/briefs
8
+ skills:
9
+ dirs: []
10
+ - slug: mechanic
11
+ readme: dist/domain.roles/mechanic/readme.md
12
+ briefs:
13
+ dirs: dist/domain.roles/mechanic/briefs
14
+ skills:
15
+ dirs: dist/domain.roles/mechanic/skills
16
+ inits:
17
+ dirs: dist/domain.roles/mechanic/inits
18
+ exec:
19
+ - dist/domain.roles/mechanic/inits/init.claude.sh