@nexusbloom/cli 0.9.1 → 0.9.2

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 (3) hide show
  1. package/README.md +1 -2
  2. package/package.json +2 -3
  3. package/TESTING.md +0 -125
package/README.md CHANGED
@@ -332,8 +332,7 @@ npm run test:watch # re-run on change
332
332
  ```
333
333
 
334
334
  The suite runs fully offline and is isolated from your real config, API key and
335
- network — both properties are enforced by guard tests, not assumed. See
336
- [TESTING.md](./TESTING.md) for the harness and its gotchas.
335
+ network — both properties are enforced by guard tests, not assumed.
337
336
 
338
337
  ## Security Notes
339
338
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,7 @@
9
9
  "files": [
10
10
  "src/",
11
11
  "package.json",
12
- "README.md",
13
- "TESTING.md"
12
+ "README.md"
14
13
  ],
15
14
  "publishConfig": {
16
15
  "access": "public"
package/TESTING.md DELETED
@@ -1,125 +0,0 @@
1
- # Testing the CLI
2
-
3
- The test suite runs entirely offline and never touches a developer's real
4
- config, credentials or network. Both of those properties are enforced, not
5
- assumed — see [Leak guards](#leak-guards).
6
-
7
- ```bash
8
- npm test # run everything
9
- npm run test:coverage # run everything with a coverage report
10
- npm run test:watch # re-run on change
11
- node --test --import ./test/setup.mjs test/pipe.test.js # one file
12
- ```
13
-
14
- ## The isolation harness
15
-
16
- Every test file is launched with `--import ./test/setup.mjs`. That module runs
17
- **before any `src/` file is loaded**, which is the only moment it can work:
18
- `paths.js` and `API_BASE` are evaluated at import time, so isolation set up
19
- inside a test file would already be too late.
20
-
21
- `test/setup.mjs` gives each test file:
22
-
23
- | | |
24
- |---|---|
25
- | `XDG_CONFIG_HOME` / `XDG_CACHE_HOME` | a fresh `mkdtemp` directory, so tests read and write a throwaway config instead of yours |
26
- | `HOME` | masked, so any fallback path cannot reach your real home |
27
- | `NEXUSBLOOM_API_URL` | `https://api.nxb.invalid` — `.invalid` is reserved by RFC 2606, so a request that escapes a stub fails instead of reaching production |
28
- | `NEXUSBLOOM_API_KEY`, `NEXUSBLOOM_SUPABASE_ANON_KEY` | deleted, so a test can never spend your key |
29
- | `NXB_NO_INTERACTIVE`, `CI` | set, so no prompt can block a run |
30
- | `NEXUSBLOOM_TELEMETRY` | deleted, so no test can phone home |
31
- | `process.stdin` | replaced with a double that is **not** a TTY but does emit `end` |
32
-
33
- That last row is subtle and load-bearing. `readStdin()` resolves on `end`, or
34
- immediately when stdin is a TTY. Under `node --test` stdin is a pipe that is
35
- never closed, so `end` never arrives and any test reaching `readStdin()` waits
36
- forever — which surfaces as an 800-second file timeout rather than a failing
37
- assertion. The double has to satisfy three things at once: stay non-TTY so
38
- `isInteractive()` is correctly false, deliver `end` to a listener that attaches
39
- later, and hold no OS handle so the file can still exit. A test that wants piped
40
- input uses `withStdin()`.
41
-
42
- ## Helpers
43
-
44
- | helper | use |
45
- |---|---|
46
- | `helpers/net.js` | `resetNetwork()` · `route/json/text/status` · `calledWith` · `calls` |
47
- | `helpers/output.js` | `captureOutput(fn)` · `captureBoth(fn)` → `{value, stdout, stderr}` |
48
- | `helpers/cli.js` | `withExitCode(fn)` → `{value, exitCode}` |
49
- | `helpers/stdin.js` | `withStdin(text, fn)` |
50
- | `helpers/capture.js` | `captureIO` — string chunks only |
51
- | `helpers/ui-double.mjs` | scripted stand-in for the interactive prompts |
52
- | `helpers/ui-hook.mjs` | `module.register` hook that redirects only `run.js`'s `./ui.js` import |
53
-
54
- ## Gotchas
55
-
56
- Each of these cost real debugging time. They are properties of the runner and
57
- the harness, not opinions.
58
-
59
- **`node --test` writes its own protocol to stdout as Buffers.** A capture helper
60
- that decodes Buffers into the captured text corrupts it, and a
61
- `JSON.parse(captured)` fails intermittently with `Unexpected token` depending on
62
- timing. Record string chunks only and pass binary straight through. Both
63
- `helpers/output.js` and `helpers/capture.js` do this.
64
-
65
- **Do not capture stdout around anything that awaits a real timer or async gap.**
66
- The runner's own reporting written during that window is swallowed, and the file
67
- reports a wrong final test count with no error at all.
68
-
69
- **Wrap anything that reports a failure in `withExitCode`.** The CLI signals
70
- failure by setting `process.exitCode = 1` and returning, so the caller can keep
71
- working. That is right for the CLI and wrong for a test: a perfectly correct test
72
- of a failure path would make the whole file exit non-zero.
73
-
74
- **`resetNetwork()` between cases, not just between tests.** Routes only append
75
- and the first match wins, so registering two responses for one pattern leaves
76
- the second as dead code and every assertion after it passes for the wrong reason.
77
-
78
- **`await import("node:fs")` yields a namespace, not a live binding.** A stub
79
- assigned to the *default* export is invisible to a named-export read from inside
80
- the module. To exercise a path that checks the filesystem, change the
81
- filesystem — e.g. `sandbox-missing.test.js` copies `sandbox.js` into a temp
82
- directory with no `runner.js` beside it, which is a real broken install.
83
-
84
- **Module-level state latches.** `runnerMissing` in `sandbox.js` stays true once
85
- set, so that behaviour lives in its own test file; `node --test` gives each file
86
- a separate process, which is the isolation it needs.
87
-
88
- **Interactive code is tested by injection, not by a terminal.** Inquirer cannot
89
- be driven headlessly and a pending prompt keeps the process alive forever. The
90
- wizard takes a `prompts` bundle (`wizard-flow.test.js`); `run.js`'s interactive
91
- half is reached through a `module.register` hook that swaps only its `./ui.js`
92
- import (`run-interactive.test.js`). Both keep the real decision logic in `src`
93
- and replace only the presentation.
94
-
95
- **`index.js` is spawned, not imported.** It parses argv on import. Spawning it
96
- is also the honest way to assert the two contracts users depend on: exit codes,
97
- and the stdout/stderr split.
98
-
99
- ## Writing a test for a new module
100
-
101
- 1. `import` it normally at the top of the file. The `--import` hook has already
102
- run, so isolation is in place.
103
- 2. Network: `beforeEach(() => resetNetwork())`, `after(() => restoreNetwork())`.
104
- 3. Failure paths: wrap in `withExitCode` and assert `exitCode`.
105
- 4. Anything reading stdin: `withStdin`, or just let the default resolve `""`.
106
- 5. Assert behaviour, not coverage. A test that exists only to execute a line is
107
- a maintenance liability that fails for the wrong reasons.
108
-
109
- ## Leak guards
110
-
111
- `test/isolation.test.js` is not an ordinary unit test file. Each test fails if a
112
- future change lets the suite read the developer's real config, reuse a real API
113
- key, or reach the real network:
114
-
115
- - config and cache resolve inside the temp sandbox, never under the real home
116
- - no inherited credentials or telemetry opt-in survive into the process
117
- - the API base is unroutable, so an unstubbed request fails rather than escaping
118
- - a canary secret never appears in captured stdout or stderr
119
- - redacted config leaks neither the value nor a fragment of it
120
- - `config.json` is `0600` inside a `0700` directory
121
- - no source file contains a hardcoded credential
122
- - no inlined JWT grants more than the `anon` role
123
-
124
- If you add a code path that could print a secret, add a canary test for it in the
125
- same change.