opencode-swarm 7.196.0 → 7.197.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 (129) hide show
  1. package/.opencode/skills/brainstorm/SKILL.md +2 -1
  2. package/.opencode/skills/ci-failure-batching/SKILL.md +2 -1
  3. package/.opencode/skills/ci-fix-monitor/SKILL.md +2 -1
  4. package/.opencode/skills/clarify/SKILL.md +2 -1
  5. package/.opencode/skills/clarify-spec/SKILL.md +2 -1
  6. package/.opencode/skills/codebase-review-swarm/SKILL.md +1 -1
  7. package/.opencode/skills/commit-pr/SKILL.md +3 -2
  8. package/.opencode/skills/consult/SKILL.md +2 -1
  9. package/.opencode/skills/council/SKILL.md +2 -1
  10. package/.opencode/skills/critic-gate/SKILL.md +2 -1
  11. package/.opencode/skills/deep-dive/SKILL.md +2 -1
  12. package/.opencode/skills/deep-research/SKILL.md +2 -1
  13. package/.opencode/skills/design-docs/SKILL.md +2 -1
  14. package/.opencode/skills/discover/SKILL.md +2 -1
  15. package/.opencode/skills/durable-session-state/SKILL.md +2 -1
  16. package/.opencode/skills/engineering-conventions/SKILL.md +2 -1
  17. package/.opencode/skills/execute/SKILL.md +2 -1
  18. package/.opencode/skills/fork-pr-operations/SKILL.md +2 -1
  19. package/.opencode/skills/gate-attribution/SKILL.md +2 -1
  20. package/.opencode/skills/issue-ingest/SKILL.md +2 -1
  21. package/.opencode/skills/issue-tracer/SKILL.md +1 -1
  22. package/.opencode/skills/loop/SKILL.md +2 -1
  23. package/.opencode/skills/merge-queue-readiness/SKILL.md +2 -1
  24. package/.opencode/skills/orchestrating-subagents/SKILL.md +2 -1
  25. package/.opencode/skills/parallel-work-check/SKILL.md +17 -16
  26. package/.opencode/skills/phase-wrap/SKILL.md +2 -1
  27. package/.opencode/skills/pre-phase-briefing/SKILL.md +2 -1
  28. package/.opencode/skills/running-tests/SKILL.md +2 -1
  29. package/.opencode/skills/skill-edit-validation/SKILL.md +2 -1
  30. package/.opencode/skills/specify/SKILL.md +2 -1
  31. package/.opencode/skills/swarm/SKILL.md +2 -1
  32. package/.opencode/skills/swarm-ci-monitor/SKILL.md +3 -2
  33. package/.opencode/skills/swarm-implement/SKILL.md +2 -1
  34. package/.opencode/skills/swarm-plan/SKILL.md +2 -1
  35. package/.opencode/skills/swarm-pr-feedback/SKILL.md +9 -162
  36. package/.opencode/skills/swarm-pr-feedback/references/gate-evidence-scope.md +20 -0
  37. package/.opencode/skills/swarm-pr-feedback/references/intake-surfaces.md +107 -0
  38. package/.opencode/skills/swarm-pr-feedback/references/preflight-checkout.md +67 -0
  39. package/.opencode/skills/swarm-pr-review/SKILL.md +12 -344
  40. package/.opencode/skills/swarm-pr-review/references/council-mode.md +38 -0
  41. package/.opencode/skills/swarm-pr-review/references/explorer-output-format.md +54 -0
  42. package/.opencode/skills/swarm-pr-review/references/findings-persistence-detail.md +15 -0
  43. package/.opencode/skills/swarm-pr-review/references/parser-candidate-extraction.md +75 -0
  44. package/.opencode/skills/swarm-pr-review/references/phase0a-intake.md +61 -0
  45. package/.opencode/skills/swarm-pr-review/references/scope-detection.md +103 -0
  46. package/.opencode/skills/swarm-pr-review/references/writeback-phases.md +68 -0
  47. package/.opencode/skills/swarm-pr-subscribe/SKILL.md +3 -2
  48. package/.opencode/skills/swarm-resume/SKILL.md +2 -1
  49. package/.opencode/skills/test-file-split/SKILL.md +2 -1
  50. package/.opencode/skills/worktree-retry-cleanup/SKILL.md +2 -1
  51. package/.opencode/skills/writing-tests/SKILL.md +7 -437
  52. package/.opencode/skills/writing-tests/references/cross-platform-patterns.md +237 -0
  53. package/.opencode/skills/writing-tests/references/mock-tiers.md +207 -0
  54. package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
  55. package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
  56. package/dist/cli/{auto-checkpoint-f00tk7sc.js → auto-checkpoint-1dv9a1w5.js} +4 -4
  57. package/dist/cli/{checkpoint-acztxqga.js → checkpoint-rkjb6kpr.js} +8 -8
  58. package/dist/cli/{coder-settlement-0ka4n1xf.js → coder-settlement-eavtvg8d.js} +3 -3
  59. package/dist/cli/{config-doctor-qv7wc6fa.js → config-doctor-4ck8e1fq.js} +7 -7
  60. package/dist/cli/{core-dmn3arr1.js → core-cyj0vphv.js} +1 -1
  61. package/dist/cli/{curator-drift-sxzs48ew.js → curator-drift-15mcxawb.js} +5 -5
  62. package/dist/cli/{curator-llm-factory-cvrt4bf9.js → curator-llm-factory-k0yebhsk.js} +25 -25
  63. package/dist/cli/{curator-891pc8b7.js → curator-p1r777m5.js} +25 -25
  64. package/dist/cli/{doctor-knowledge-receipt-phase-id-mvbj576e.js → doctor-knowledge-receipt-phase-id-k204028d.js} +4 -4
  65. package/dist/cli/{evidence-summary-service-x9nxvmn5.js → evidence-summary-service-4mx5ge81.js} +9 -9
  66. package/dist/cli/{gate-evidence-dxd20wrc.js → gate-evidence-pa8qadxh.js} +2 -2
  67. package/dist/cli/{guardrail-explain-fga9bnvt.js → guardrail-explain-pba5em4h.js} +26 -26
  68. package/dist/cli/{guardrail-log-y84b9m8n.js → guardrail-log-24fbykv3.js} +3 -3
  69. package/dist/cli/{guardrail-reset-xpcsxyt0.js → guardrail-reset-rfnr2vnn.js} +25 -25
  70. package/dist/cli/{hive-promoter-w1vchgfr.js → hive-promoter-0ht8jafr.js} +25 -25
  71. package/dist/cli/{index-hjma52c8.js → index-0sw6gjtn.js} +3 -3
  72. package/dist/cli/{index-mt5t492g.js → index-3h4tajza.js} +2 -2
  73. package/dist/cli/{index-t5med3ga.js → index-528gk6k1.js} +1 -1
  74. package/dist/cli/{index-nrqyc8pr.js → index-56n6nb7p.js} +25 -25
  75. package/dist/cli/{index-bjaef5zn.js → index-57g7yqx9.js} +390 -233
  76. package/dist/cli/{index-jnjvghgp.js → index-6g81mmmy.js} +4 -4
  77. package/dist/cli/{index-8bsnnnj2.js → index-9rqvm5sk.js} +2 -2
  78. package/dist/cli/{index-ryjp0wmq.js → index-9vwsvgzp.js} +1 -1
  79. package/dist/cli/{index-knxq5byr.js → index-a9bs0c71.js} +1 -1
  80. package/dist/cli/{index-3hpjqa6y.js → index-d0m3snqt.js} +1 -1
  81. package/dist/cli/{index-7zkgdefv.js → index-d483e4gz.js} +3 -3
  82. package/dist/cli/{index-sgt1d7ez.js → index-efen9p4c.js} +1 -1
  83. package/dist/cli/{index-w3mav870.js → index-f0d3z2ha.js} +2 -2
  84. package/dist/cli/{index-wscg5ryb.js → index-fmgkemn9.js} +1 -1
  85. package/dist/cli/{index-svbh91pz.js → index-h69nppa3.js} +1 -1
  86. package/dist/cli/{index-391ydd25.js → index-hyxw4j9t.js} +1 -1
  87. package/dist/cli/{index-41p9zzpe.js → index-kah11mjw.js} +153 -52
  88. package/dist/cli/{index-nqxbqb50.js → index-mwh6srje.js} +1 -1
  89. package/dist/cli/{index-psqfkgav.js → index-pek9gmwm.js} +3 -3
  90. package/dist/cli/{index-31b55cq9.js → index-pywrvjxj.js} +3 -3
  91. package/dist/cli/{index-2grr37ry.js → index-qae4572e.js} +1 -1
  92. package/dist/cli/{index-tzasccpd.js → index-qqe9tt5y.js} +1 -1
  93. package/dist/cli/{index-4cazf4kb.js → index-r9ab1sat.js} +27 -27
  94. package/dist/cli/{index-174j0g38.js → index-rqbhn0mh.js} +2 -2
  95. package/dist/cli/{index-te61qv0q.js → index-syrktqz9.js} +2 -2
  96. package/dist/cli/{index-3drq02r1.js → index-t4nxee8x.js} +1 -1
  97. package/dist/cli/{index-ppnpkww2.js → index-tstk9xx7.js} +1 -1
  98. package/dist/cli/{index-tjc68trf.js → index-vanjqbhp.js} +5 -5
  99. package/dist/cli/{index-vsa05smc.js → index-xtb61w73.js} +1 -1
  100. package/dist/cli/{index-7gqmqhzj.js → index-z5rkkhng.js} +6 -6
  101. package/dist/cli/index.js +26 -26
  102. package/dist/cli/{knowledge-escalator-2dy96d0j.js → knowledge-escalator-89crpby4.js} +7 -7
  103. package/dist/cli/{knowledge-events-70kb745b.js → knowledge-events-eyratqdp.js} +5 -5
  104. package/dist/cli/{knowledge-store-hvq03g0j.js → knowledge-store-kkwds05z.js} +1 -1
  105. package/dist/cli/{knowledge-validator-eaatcnkr.js → knowledge-validator-fww6p37v.js} +2 -2
  106. package/dist/cli/{mcp-bj3awdfy.js → mcp-8t3m998m.js} +1 -1
  107. package/dist/cli/{model-preflight-skhq4xtr.js → model-preflight-98g183my.js} +25 -25
  108. package/dist/cli/{pending-delegations-yamzv468.js → pending-delegations-qj5e0hh2.js} +2 -2
  109. package/dist/cli/{pr-subscriptions-hkzd0nr4.js → pr-subscriptions-edwd3syw.js} +1 -1
  110. package/dist/cli/{pr-workflow-gate-2gmznmj4.js → pr-workflow-gate-t8veygyh.js} +25 -25
  111. package/dist/cli/{scan-cursor-arezehhk.js → scan-cursor-0ffhpz79.js} +2 -2
  112. package/dist/cli/{schema-emx6r32d.js → schema-zytt0t7a.js} +1 -1
  113. package/dist/cli/{scope-persistence-tvr8w7ea.js → scope-persistence-p8a9a2xb.js} +5 -5
  114. package/dist/cli/{server-ez03wdw9.js → server-q4b9s64v.js} +25 -25
  115. package/dist/cli/{skill-generator-w0jy1mpc.js → skill-generator-wt7d640t.js} +8 -8
  116. package/dist/cli/{snapshot-coordination-init-2x56ds8a.js → snapshot-coordination-init-ywxyf130.js} +25 -25
  117. package/dist/cli/{speckit-checkoff-c2e5wezr.js → speckit-checkoff-y8na83hj.js} +3 -3
  118. package/dist/cli/{worktree-collision-ownership-1bgcea0v.js → worktree-collision-ownership-kssmhrpk.js} +2 -2
  119. package/dist/cli/{worktree-isolation-9xp4dz41.js → worktree-isolation-w6np680v.js} +25 -25
  120. package/dist/config/skill-contract-digest.d.ts +6 -0
  121. package/dist/hooks/pr-workflow-gate.d.ts +12 -0
  122. package/dist/hooks/pr-workflow-response-gate.d.ts +8 -0
  123. package/dist/hooks/skill-scoring.d.ts +1 -1
  124. package/dist/hooks/skill-usage-log.d.ts +2 -0
  125. package/dist/index.js +30 -28
  126. package/dist/pr-review/persistence.d.ts +18 -1
  127. package/dist/services/skill-evaluator.d.ts +2 -0
  128. package/dist/services/skill-generator.d.ts +13 -0
  129. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ # Cross-Platform Test Patterns and Requirements
2
+
3
+ ## Cross-Platform Requirements
4
+
5
+ > **See also**: [Cross-Platform Test Patterns](#cross-platform-test-patterns) above for detailed
6
+ > guidance on mock keys, symlink behavior, temp directories, and line endings.
7
+
8
+ All tests must pass on Linux, macOS, and Windows unless explicitly gated with:
9
+ ```typescript
10
+ const isWindows = process.platform === 'win32';
11
+ if (isWindows) test.skip('reason', () => {});
12
+ ```
13
+
14
+ ### Path handling
15
+ - Use `path.join()` or `path.resolve()`, never string concatenation with `/`.
16
+ - Temp directories: use `os.tmpdir()`, not hardcoded `/tmp`.
17
+ - File comparisons: normalize paths before comparing (`path.resolve(a) === path.resolve(b)`).
18
+
19
+ ### Process spawning
20
+ - Use `.cmd` extension on Windows for npm/bun binaries: `process.platform === 'win32' ? 'bun.cmd' : 'bun'`.
21
+ - Use array-form `spawn`/`spawnSync`, never shell string commands.
22
+
23
+ ### macOS rename-visibility race (write-then-read atomic files)
24
+
25
+ On macOS/APFS, `fs.renameSync` can complete before the data is visible to
26
+ subsequent reads. Tests that write-then-read atomic files may fail on
27
+ `macos-latest` but pass on `ubuntu-latest` and `windows-latest`.
28
+
29
+ **Symptom:** Test fails only on macOS with `result === null` or
30
+ `result === undefined` immediately after a write that should have made the
31
+ file visible.
32
+
33
+ **Root cause:** macOS filesystem updates the directory entry asynchronously
34
+ after `fs.renameSync`. Immediately-following reads may see ENOENT or stale
35
+ data.
36
+
37
+ **Fix — three layers (use all three for production code):**
38
+
39
+ **Layer 1: Use `bunWrite` for atomic writes.** The `bunWrite` function in
40
+ `src/utils/bun-compat.ts` already handles temp file creation, fsync,
41
+ atomic rename, and parent directory fsync correctly across platforms.
42
+ Do NOT reimplement this pattern.
43
+
44
+ **Layer 2: Add ENOENT retry in the read path.** Wrap `validateSwarmPath` and
45
+ the file read in a try/catch with a bounded retry loop for ENOENT:
46
+
47
+ ```typescript
48
+ // CORRECT — retry on ENOENT only (not other errors), bounded
49
+ const maxAttempts = 5;
50
+ const retryDelayMs = 10;
51
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
52
+ try {
53
+ const resolvedPath = _internals.validateSwarmPath(directory, filename);
54
+ const file = bunFile(resolvedPath);
55
+ const content = await file.text();
56
+ return content;
57
+ } catch (err) {
58
+ const isNotFound = (err as NodeJS.ErrnoException)?.code === 'ENOENT';
59
+ if (!isNotFound || attempt === maxAttempts - 1) {
60
+ return null;
61
+ }
62
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
63
+ }
64
+ }
65
+ return null;
66
+ ```
67
+
68
+ CRITICAL: `validateSwarmPath` must be INSIDE the try block so that throws
69
+ (for path traversal attempts) are caught and return null. Security tests
70
+ expect `readSwarmFileAsync` to return null for traversal attempts.
71
+
72
+ **Layer 3: Don't add arbitrary delays.** `setTimeout` should be bounded
73
+ (5-10ms, max 5 attempts). Do not add `await new Promise(r => setTimeout(r, 100))`
74
+ "just in case" — that's a code smell. The retry loop handles it.
75
+
76
+ See [`.opencode/skills/engineering-conventions/SKILL.md`](../engineering-conventions/SKILL.md)
77
+ for the evidence file flow that triggers this retry pattern in QA gates.
78
+
79
+ ### Node FileHandle API
80
+
81
+ Node's `FileHandle` uses `.sync()`, NOT `.fsync()`:
82
+
83
+ ```typescript
84
+ // CORRECT
85
+ const fd = await fsPromises.open(dir, 'r');
86
+ try {
87
+ await fd.sync();
88
+ } finally {
89
+ await fd.close();
90
+ }
91
+
92
+ // WRONG — TypeScript error: Property 'fsync' does not exist on type 'FileHandle'
93
+ const fd = await fsPromises.open(dir, 'r');
94
+ try {
95
+ await fd.fsync();
96
+ } finally {
97
+ await fd.close();
98
+ }
99
+ ```
100
+
101
+ ## Cross-Platform Test Patterns
102
+
103
+ Tests run on all three CI platforms (ubuntu, macos, windows). Path and filesystem behavior
104
+ differs between them. Follow these patterns to prevent platform-specific failures:
105
+
106
+ ### Mock keys with filesystem paths
107
+
108
+ **Never hardcode Unix-format paths as mock keys.** On Windows, `path.resolve('/dir', 'file')`
109
+ produces drive-letter-prefixed paths like `D:\dir\file`, not `/dir/file`. A mock that checks
110
+ for `/dir/file` will silently never match, causing the test to behave differently on Windows.
111
+
112
+ **Use `path.resolve()` to construct mock keys the same way the source code does:**
113
+
114
+ ```typescript
115
+ // ❌ WRONG — fails on Windows (mock expects '/safe/dir/linked.ts',
116
+ // but path.resolve('/safe/dir', 'linked.ts') = 'D:\safe\dir\linked.ts')
117
+ mockRealpathSync.mockImplementation((inputPath: string) => {
118
+ if (inputPath === '/safe/dir') return '/safe/dir';
119
+ if (inputPath === '/safe/dir/linked.ts') return '/outside/linked.ts';
120
+ return inputPath;
121
+ });
122
+
123
+ // ✅ CORRECT — path.resolve produces matching keys on all platforms
124
+ const mockDir = path.resolve('/safe/dir');
125
+ const linkedResolved = path.resolve(mockDir, 'linked.ts');
126
+ const outsideResolved = path.resolve('/outside/linked.ts');
127
+
128
+ // mockRealpathSync is a mock() function (bun:test) — see mocking patterns above
129
+ mockRealpathSync.mockImplementation((inputPath: string) => {
130
+ if (inputPath === mockDir) return mockDir;
131
+ if (inputPath === linkedResolved) return outsideResolved;
132
+ return inputPath;
133
+ });
134
+ ```
135
+
136
+ ### Symlink behavior differences
137
+
138
+ - On Windows, `fs.symlinkSync` for directories creates **junctions** by default, which
139
+ resolve differently than POSIX symlinks. Junction creation may require administrator
140
+ elevation on older Node.js versions.
141
+ - `fs.realpathSync` on a broken symlink throws `ENOENT` on POSIX but may throw
142
+ `EINVAL` on Windows, depending on symlink type.
143
+ - Use `test.skipIf(process.platform === 'win32')` for tests that directly manipulate
144
+ filesystem symlinks, unless the test's purpose is explicitly to verify cross-platform
145
+ symlink behavior.
146
+
147
+ ### Temporary directory patterns
148
+
149
+ - Use `os.tmpdir()` + `path.join()` for temp paths. **Never** hardcode `/tmp` or `C:\`.
150
+ - Wrap `mkdtempSync` in `realpathSync` if the result is `chdir`'d on macOS (temp
151
+ dirs are often symlinked to `/private/var/...`).
152
+ - Clean up temp dirs in `afterEach` or `afterAll` with a bounded helper that
153
+ verifies the resolved cleanup target is a child of `os.tmpdir()` before
154
+ calling recursive `rm`. Reuse `tests/helpers/safe-test-dir.ts` when possible.
155
+ Do not call recursive `rm` on a computed path unless the helper has rejected
156
+ empty strings, `os.tmpdir()` itself, and paths outside the temp root.
157
+
158
+ ### Platform-specific environment variable redirection
159
+
160
+ When tests redirect `process.env.HOME` to isolate path-resolver-dependent code
161
+ (functions like `resolveHiveKnowledgePath`, `resolveSwarmKnowledgePath`, or any
162
+ function that reads `os.homedir()` / platform env vars), they MUST redirect ALL
163
+ platform-specific env vars, not just `HOME`. A partial redirect silently falls
164
+ back to the real user profile on some platforms, causing tests to read/write
165
+ actual user data instead of the isolated temp directory.
166
+
167
+ Per-platform requirements:
168
+
169
+ - **Linux**: redirect `HOME`, `XDG_CONFIG_HOME`, and `XDG_DATA_HOME`.
170
+ - **macOS**: redirect `HOME` (macOS resolves `~/Library/Application Support` from
171
+ the home directory).
172
+ - **Windows**: redirect `HOME`, `LOCALAPPDATA`, AND `APPDATA`. Windows path
173
+ resolvers read `LOCALAPPDATA` and `APPDATA`, neither of which is derived from
174
+ `HOME`. Redirecting only `HOME` silently fails on Windows, causing tests to
175
+ touch the real `%LOCALAPPDATA%` and `%APPDATA%` trees.
176
+
177
+ > **⚠️ Bun caches `os.homedir()` on first call.** If a module calls `os.homedir()`
178
+ > before the test sets `process.env.HOME`, the cached value persists for the
179
+ > lifetime of the process and later env changes are silently ignored. Set
180
+ > `process.env.HOME` (and other redirected vars) **before** importing any module
181
+ > that calls `os.homedir()`. The source code documents this at
182
+ > `src/hooks/knowledge-store.ts`: "Bun caches os.homedir(), so changing $HOME
183
+ > after first call is ignored."
184
+
185
+ Use per-variable save/restore rather than saving and replacing the entire
186
+ `process.env` object — the latter discards process-level env state and can
187
+ interfere with other test infrastructure:
188
+
189
+ ```typescript
190
+ import { beforeEach, afterEach } from 'bun:test';
191
+ import os from 'node:os';
192
+ import path from 'node:path';
193
+
194
+ const saved = {
195
+ HOME: process.env.HOME,
196
+ LOCALAPPDATA: process.env.LOCALAPPDATA,
197
+ APPDATA: process.env.APPDATA,
198
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
199
+ XDG_DATA_HOME: process.env.XDG_DATA_HOME,
200
+ };
201
+
202
+ beforeEach(() => {
203
+ const isolatedDir = path.join(os.tmpdir(), 'test-home');
204
+ process.env.HOME = isolatedDir;
205
+ process.env.LOCALAPPDATA = isolatedDir;
206
+ process.env.APPDATA = isolatedDir;
207
+ process.env.XDG_CONFIG_HOME = isolatedDir;
208
+ process.env.XDG_DATA_HOME = isolatedDir;
209
+ });
210
+
211
+ afterEach(() => {
212
+ for (const [key, value] of Object.entries(saved)) {
213
+ if (value === undefined) delete process.env[key];
214
+ else process.env[key] = value;
215
+ }
216
+ });
217
+ ```
218
+
219
+ For cross-file isolation (tests that must survive across multiple files in the
220
+ same process, e.g. batch steps), use `beforeAll` / `afterAll` with the same
221
+ per-var save/restore pattern. Never mutate `process.env` without restoring it in
222
+ a matching teardown hook.
223
+
224
+ **Preferred approach:** Use `createIsolatedTestEnv()` from
225
+ `tests/helpers/isolated-test-env.ts`. It handles `XDG_CONFIG_HOME`, `APPDATA`,
226
+ `LOCALAPPDATA`, and `HOME` with correct per-variable save/restore and returns a
227
+ cleanup function that removes the temp directory. Use this helper unless your
228
+ test has specific requirements it doesn't cover.
229
+
230
+ ### Line ending normalization
231
+
232
+ Git on Windows converts LF to CRLF by default. Tests that compare file contents
233
+ byte-by-byte against expected strings must normalize line endings:
234
+
235
+ ```typescript
236
+ const actual = readFileSync(path, 'utf-8').replace(/\r\n/g, '\n');
237
+ ```
@@ -0,0 +1,207 @@
1
+ # Two-Tier Mock Convention and mock.module() Export Completeness
2
+
3
+ ## mock.module() Export Completeness
4
+
5
+ When using `mock.module()` (or `vi.mock()`) with Bun's test runner, the mock factory **MUST provide stubs for ALL named exports** of the target module — not just the ones your test calls. Bun validates the export set at dynamic-import time and throws `SyntaxError: Export named 'X' not found` if any export is missing.
6
+
7
+ ### Why this matters
8
+
9
+ Transitive imports may reference exports your test never calls directly. For example, if your test mocks `config/schema.js` and only uses `stripKnownSwarmPrefix`, but a transitive dependency imports `PluginConfigSchema` from the same module, the mock MUST include `PluginConfigSchema` as a stub — even though your test never calls it.
10
+
11
+ When the source module gains new exports (e.g., a PR adds 50 new Zod schemas to `config/schema.ts`), ALL existing `mock.module()` calls targeting that module must be updated — even if the new exports are irrelevant to your test.
12
+
13
+ ### How to verify completeness
14
+
15
+ Before finalizing a test that uses `mock.module()`:
16
+
17
+ 1. List all runtime exports of the target module (type-only exports are erased at compile time and need no stub):
18
+ ```bash
19
+ grep -E "^export (const|function|async function|class) " src/path/to/module.ts
20
+ ```
21
+ **Note:** Do NOT include `type` or `interface` exports — Bun erases these at compile time and they need no runtime stub.
22
+ 2. Ensure every export name has an entry in your `mock.module()` factory.
23
+ 3. Stubs can be minimal:
24
+ - Functions: `() => null` or `async () => {}`
25
+ - Zod schemas: use a comprehensive stub that supports common methods:
26
+ ```typescript
27
+ const zodStub = {
28
+ parse: (v: unknown) => v,
29
+ safeParse: (v: unknown) => ({ success: true as const, data: v }),
30
+ parseAsync: async (v: unknown) => v,
31
+ };
32
+ ```
33
+ - Constants: appropriate zero values (`''`, `0`, `null`, `[]`, `{}`)
34
+
35
+ ### Verification pattern
36
+
37
+ ```typescript
38
+ // ✅ CORRECT — all exports provided, test uses only the first one
39
+ mock.module('../../../src/config/schema.js', () => ({
40
+ // The one export your test actually uses
41
+ stripKnownSwarmPrefix: mockStripFn,
42
+ // Stubs for transitive import resolution (never called in test)
43
+ PluginConfigSchema: zodStub,
44
+ ScoringConfigSchema: zodStub,
45
+ isKnownCanonicalRole: () => false,
46
+ // ... all other runtime exports as stubs
47
+ }));
48
+
49
+ // ❌ WRONG — missing exports cause SyntaxError at module-load time
50
+ mock.module('../../../src/config/schema.js', () => ({
51
+ stripKnownSwarmPrefix: mockStripFn,
52
+ // Missing: PluginConfigSchema, ScoringConfigSchema, etc.
53
+ // → "SyntaxError: Export named 'PluginConfigSchema' not found"
54
+ }));
55
+ ```
56
+
57
+ ### What IS and IS NOT test theater
58
+
59
+ Adding stubs for ESM resolution is NOT test theater — it's a Bun runtime requirement. The distinction:
60
+
61
+ | Pattern | Test theater? | Why |
62
+ |---------|--------------|-----|
63
+ | Adding `PluginConfigSchema: zodStub` so the module loads | **No** | Required for ESM resolution; stub is never called |
64
+ | Stubbing `validateDirectory` to return `true` then asserting "validation works" | **Yes** | The stub bypasses the logic you should be testing |
65
+ | Using `zodStub` in assertions: `expect(zodStub.parse(input)).toBe(input)` | **Yes** | Testing the stub, not the real code |
66
+ | Adding stubs for ALL 50 Zod schemas in config/schema.ts | **No** | All are required for transitive import resolution |
67
+
68
+ The stubs exist solely to satisfy the module loader. Test assertions must verify behavior through the real-mocked functions (the ones your test actually calls), not through the stubs.
69
+
70
+ ### Files Intentionally Using File-Scoped Mocks
71
+
72
+ Some test files use top-level `mock.module` that must persist across all tests in the file. These files use `mockReset()`/`mockClear()` in `beforeEach` instead of `mock.restore()` in `afterEach`:
73
+
74
+ - `src/__tests__/preflight-phase.test.ts` — mocks `plan/manager` and `preflight-service`
75
+
76
+ ## Two-Tier Mock Convention
77
+
78
+ The codebase uses a two-tier strategy for mock isolation, plus a zero-mock testing pattern:
79
+
80
+ ### Tier 0: _test_exports Pure Function Testing (Zero Mocks)
81
+
82
+ When a module contains internal utility functions (formatters, normalizers, transformers) that don't need external dependencies, export them via a `_test_exports` object for direct unit testing. This avoids `mock.module` entirely and produces tests that are deterministic, fast, and immune to Bun's cross-file mock leakage:
83
+
84
+ ```typescript
85
+ // In source file (src/tools/formatter.ts)
86
+ function formatEntry(entry: SomeType): string {
87
+ // internal implementation — may use optional chaining, defaults, etc.
88
+ return entry.score?.toFixed(2) ?? 'N/A';
89
+ }
90
+
91
+ // Public API (tool handler, command handler, etc.)
92
+ export function handleQuery(ctx: Context) {
93
+ const entries = readData(ctx);
94
+ return entries.map(formatEntry);
95
+ }
96
+
97
+ // Export seam for testing — only used by test files
98
+ export const _test_exports = { formatEntry };
99
+ ```
100
+
101
+ ```typescript
102
+ // In test file (tests/unit/tools/formatter.test.ts)
103
+ import { _test_exports } from '../../../src/tools/formatter';
104
+
105
+ const { formatEntry } = _test_exports;
106
+
107
+ describe('formatEntry', () => {
108
+ test('handles missing score', () => {
109
+ expect(formatEntry({ score: undefined })).toBe('N/A');
110
+ });
111
+ test('formats numeric score', () => {
112
+ expect(formatEntry({ score: 0.85 })).toBe('0.85');
113
+ });
114
+ });
115
+ ```
116
+
117
+ **When to use Tier 0 vs Tier 1:**
118
+ - **Tier 0 (`_test_exports`)**: The function is a pure utility (formatter, normalizer, transformer) that doesn't call external modules. No mocking needed — test it directly.
119
+ - **Tier 1 (`_internals`)**: You need to mock a function within the same module to test the caller in isolation. The function has side effects or calls external APIs.
120
+ - **Tier 2 (`mock.module`)**: You need to mock a dependency from another module (Node built-ins, other application modules).
121
+
122
+ **Benefits of Tier 0:**
123
+ - Zero mock pollution — no `mock.module` calls, no `mock.restore()` needed
124
+ - Works in batch test runs without per-file isolation
125
+ - Type-safe (the exported object carries the real TypeScript types)
126
+ - No filesystem dependencies (no tmpDir, no chdir, no existsSync)
127
+ - Deterministic on all platforms and CI environments
128
+
129
+ ### Tier 1: _internals DI Seams (Within-Module)
130
+
131
+ For mocking functions within the same module, source files export an `_internals` object that wraps key functions. Tests can replace individual functions without using `mock.module`:
132
+
133
+ ```typescript
134
+ // In source file (src/services/my-service.ts)
135
+ export const _internals = {
136
+ helperFn: () => { /* real implementation */ }
137
+ };
138
+
139
+ export function mainFn() {
140
+ return _internals.helperFn();
141
+ }
142
+ ```
143
+
144
+ ```typescript
145
+ // In test file
146
+ import { _internals, mainFn } from '../../../src/services/my-service';
147
+
148
+ test('mainFn uses mocked helper', () => {
149
+ const original = _internals.helperFn;
150
+ _internals.helperFn = mock(() => 'mocked');
151
+ // ... test ...
152
+ _internals.helperFn = original; // restore
153
+ });
154
+ ```
155
+
156
+ **Benefits:**
157
+ - No process-global mock pollution
158
+ - Type-safe
159
+ - Fast (no module re-parsing)
160
+ - Works in batch test runs without isolation
161
+
162
+ **Critical limitation — reference-captured functions:** `_internals` interception requires the source code to read `_internals.fn` at the call site. When a function is instead passed as a direct argument or captured in a closure at module definition time, replacing `_internals.fn` has no effect — the mock is silently ignored and the real function runs.
163
+
164
+ ```typescript
165
+ // Source: readKnowledge is captured at definition time, NOT via _internals
166
+ export async function transactKnowledge(filePath: string, mutate: Fn) {
167
+ return transactFile(filePath, readKnowledge, ...); // direct ref, captured at definition time
168
+ }
169
+ export const _internals = { readKnowledge }; // mutating this does NOT affect the closure above
170
+
171
+ // Test — mock is silently ignored; real readKnowledge still runs
172
+ const orig = _internals.readKnowledge;
173
+ _internals.readKnowledge = mock(() => []); // only mutates the object property
174
+ await transactKnowledge(path, mutate); // still calls the real readKnowledge
175
+ _internals.readKnowledge = orig;
176
+ ```
177
+
178
+ When `_internals` interception cannot work, verify **observable outcomes** instead: run concurrent callers and assert on final persisted state. See `tests/unit/hooks/knowledge-application.test.ts` ("two concurrent bumpCountersBatch calls") for the pattern.
179
+
180
+ ### Tier 2: mock.module (Cross-Module)
181
+
182
+ When mocking dependencies from other modules (especially Node built-ins), use `mock.module` with proper cleanup:
183
+
184
+ ```typescript
185
+ import * as realFs from 'node:fs/promises';
186
+
187
+ mock.module('node:fs/promises', () => ({
188
+ ...realFs, // MUST spread real exports
189
+ readFile: mock(() => Promise.resolve('mocked')),
190
+ }));
191
+
192
+ afterEach(() => mock.restore());
193
+ ```
194
+
195
+ **Critical rules for cross-module mocks:**
196
+ 1. **Always spread real exports** for Node built-ins — other code depends on exports you don't mock
197
+ 2. **Always add `afterEach(mock.restore())`** — provides best-effort cleanup
198
+ 3. **Run in per-file isolation** — CI runs each file in its own process (`for f in *.test.ts; do bun --smol test "$f"; done`)
199
+
200
+ ### Choosing Between Tiers
201
+
202
+ | Scenario | Pattern | Example |
203
+ |----------|---------|--------|
204
+ | Mocking a function in the same module you're testing | `_internals` seam | `src/state.ts` `_internals.loadSnapshot` |
205
+ | Mocking a Node built-in (fs, child_process, etc.) | `mock.module` + spread real | `mock.module('node:fs/promises', () => ({ ...realFs, readFile: mockFn }))` |
206
+ | Mocking another application module | `mock.module` + cleanup | `mock.module('../../../src/utils/logger', ...)` + `afterEach(mock.restore())` |
207
+ | File-scoped mock (applies to all tests in file) | `mock.module` at top level + `mockReset()` in `beforeEach` | Preflight tests with `mockLoadPlan.mockReset()` |
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- import"./index-8bsnnnj2.js";
2
+ import"./index-9rqvm5sk.js";
3
3
  import"./index-g22dps7x.js";
4
4
  import"./index-0725jpp7.js";
5
5
  import {
@@ -15,12 +15,12 @@ import"./index-yf2z4j7s.js";
15
15
  import"./index-gx2vp8gx.js";
16
16
  import {
17
17
  loadPluginConfigWithMeta
18
- } from "./index-ppnpkww2.js";
18
+ } from "./index-tstk9xx7.js";
19
19
  import {
20
20
  init_git_executable,
21
21
  resolveGitExecutable
22
22
  } from "./index-zxdawwc7.js";
23
- import"./index-nqxbqb50.js";
23
+ import"./index-mwh6srje.js";
24
24
  import"./index-hqrkr9ag.js";
25
25
  import"./index-bk5tah7q.js";
26
26
  import"./index-gknz3bv1.js";
@@ -152,7 +152,7 @@ async function maybeSaveAutoCheckpoint(directory, plan) {
152
152
  generation += 1;
153
153
  label = buildAutoCheckpointLabel(plan, completedCount, generation);
154
154
  }
155
- const { saveCheckpointRecord } = await import("./checkpoint-acztxqga.js");
155
+ const { saveCheckpointRecord } = await import("./checkpoint-rkjb6kpr.js");
156
156
  const result = await saveCheckpointRecord(label, directory);
157
157
  if (result.success) {
158
158
  emitAutoSavedEvent(directory, {
@@ -3,21 +3,21 @@ import {
3
3
  _internals,
4
4
  checkpoint,
5
5
  saveCheckpointRecord
6
- } from "./index-31b55cq9.js";
7
- import"./index-tjc68trf.js";
6
+ } from "./index-pywrvjxj.js";
7
+ import"./index-vanjqbhp.js";
8
8
  import"./index-6b295cas.js";
9
9
  import"./index-9nyqt3kb.js";
10
- import"./index-ryjp0wmq.js";
10
+ import"./index-9vwsvgzp.js";
11
11
  import"./index-v3bmwmqw.js";
12
- import"./index-svbh91pz.js";
12
+ import"./index-h69nppa3.js";
13
13
  import"./index-g4fs6z4e.js";
14
14
  import"./index-m9zzj4ee.js";
15
15
  import"./index-kphy4we7.js";
16
16
  import"./index-2s7dvzff.js";
17
- import"./index-8bsnnnj2.js";
17
+ import"./index-9rqvm5sk.js";
18
18
  import"./index-g22dps7x.js";
19
19
  import"./index-0725jpp7.js";
20
- import"./index-174j0g38.js";
20
+ import"./index-rqbhn0mh.js";
21
21
  import"./index-6183at3v.js";
22
22
  import"./index-crz772sx.js";
23
23
  import"./index-app2rmxv.js";
@@ -33,9 +33,9 @@ import"./index-ejhxgyvm.js";
33
33
  import"./index-7g4c7s5r.js";
34
34
  import"./index-yf2z4j7s.js";
35
35
  import"./index-gx2vp8gx.js";
36
- import"./index-ppnpkww2.js";
36
+ import"./index-tstk9xx7.js";
37
37
  import"./index-zxdawwc7.js";
38
- import"./index-nqxbqb50.js";
38
+ import"./index-mwh6srje.js";
39
39
  import"./index-hqrkr9ag.js";
40
40
  import"./index-bk5tah7q.js";
41
41
  import"./index-gknz3bv1.js";
@@ -14,11 +14,11 @@ import {
14
14
  releaseCoderDispatchOwnership,
15
15
  settleCoderDispatch,
16
16
  transferCoderSettlementToBackground
17
- } from "./index-7zkgdefv.js";
17
+ } from "./index-d483e4gz.js";
18
18
  import"./index-ghckhk16.js";
19
19
  import"./index-dfrcc46h.js";
20
- import"./index-3drq02r1.js";
21
- import"./index-391ydd25.js";
20
+ import"./index-t4nxee8x.js";
21
+ import"./index-hyxw4j9t.js";
22
22
  import"./index-g4fs6z4e.js";
23
23
  import"./index-kphy4we7.js";
24
24
  import"./index-2s7dvzff.js";
@@ -21,17 +21,17 @@ import {
21
21
  shouldRunOnStartup,
22
22
  writeBackupArtifact,
23
23
  writeDoctorArtifact
24
- } from "./index-hjma52c8.js";
25
- import"./index-tjc68trf.js";
24
+ } from "./index-0sw6gjtn.js";
25
+ import"./index-vanjqbhp.js";
26
26
  import"./index-6b295cas.js";
27
- import"./index-ryjp0wmq.js";
27
+ import"./index-9vwsvgzp.js";
28
28
  import"./index-v3bmwmqw.js";
29
- import"./index-svbh91pz.js";
29
+ import"./index-h69nppa3.js";
30
30
  import"./index-m9zzj4ee.js";
31
31
  import"./index-kphy4we7.js";
32
32
  import"./index-2s7dvzff.js";
33
33
  import"./index-0725jpp7.js";
34
- import"./index-174j0g38.js";
34
+ import"./index-rqbhn0mh.js";
35
35
  import"./index-6183at3v.js";
36
36
  import"./index-crz772sx.js";
37
37
  import"./index-app2rmxv.js";
@@ -47,9 +47,9 @@ import"./index-ejhxgyvm.js";
47
47
  import"./index-7g4c7s5r.js";
48
48
  import"./index-yf2z4j7s.js";
49
49
  import"./index-gx2vp8gx.js";
50
- import"./index-ppnpkww2.js";
50
+ import"./index-tstk9xx7.js";
51
51
  import"./index-zxdawwc7.js";
52
- import"./index-nqxbqb50.js";
52
+ import"./index-mwh6srje.js";
53
53
  import"./index-hqrkr9ag.js";
54
54
  import"./index-bk5tah7q.js";
55
55
  import"./index-gknz3bv1.js";
@@ -14,7 +14,7 @@ import {
14
14
  resolveWorktreeBaseDir,
15
15
  shortenWorktreePath,
16
16
  writeLaneProfileToDiskReal
17
- } from "./index-391ydd25.js";
17
+ } from "./index-hyxw4j9t.js";
18
18
  import"./index-kphy4we7.js";
19
19
  import"./index-2s7dvzff.js";
20
20
  import"./index-fzpt2djt.js";
@@ -5,9 +5,9 @@ import {
5
5
  readPriorDriftReports,
6
6
  runDeterministicDriftCheck,
7
7
  writeDriftReport
8
- } from "./index-sgt1d7ez.js";
9
- import"./index-ryjp0wmq.js";
10
- import"./index-svbh91pz.js";
8
+ } from "./index-efen9p4c.js";
9
+ import"./index-9vwsvgzp.js";
10
+ import"./index-h69nppa3.js";
11
11
  import"./index-kphy4we7.js";
12
12
  import"./index-2s7dvzff.js";
13
13
  import"./index-0725jpp7.js";
@@ -18,9 +18,9 @@ import"./index-fzpt2djt.js";
18
18
  import"./index-ejhxgyvm.js";
19
19
  import"./index-7g4c7s5r.js";
20
20
  import"./index-yf2z4j7s.js";
21
- import"./index-ppnpkww2.js";
21
+ import"./index-tstk9xx7.js";
22
22
  import"./index-zxdawwc7.js";
23
- import"./index-nqxbqb50.js";
23
+ import"./index-mwh6srje.js";
24
24
  import"./index-hqrkr9ag.js";
25
25
  import"./index-bk5tah7q.js";
26
26
  import"./index-gknz3bv1.js";