Skip to content

Commit ac46a5a

Browse files
committed
fix(doctor): an absent optional package is a skipped check, not a pass
Two pre-existing issues, both older than the probe rework in #883 and untouched by it. `stash doctor` ended with "All checks passed." when `@cipherstash/stack` was absent. It is an optional peer, so this is the plain `npx stash doctor` path in a project that has not run `stash init` — the row said "not installed (run `stash init`)" and the outro then claimed a pass for a check that never executed. It now ends with `checksIncomplete`, the line the too-old-to-probe arm already used for exactly this distinction, and still exits 0: absence there is recoverable, not a failure. The row stays green for the same reason. The probe labels move to `messages.ts`. Both E2E suites match a row as `<label> — <detail>`; the detail halves were already constants while the labels were copied into each suite, one of them under a comment conceding the drift. `packages/cli/AGENTS.md` asks for exactly this: strings tests assert on live in `messages.ts`, and its "only when a test asserts on it" caveat is now satisfied. Verified non-vacuous: reverting the one-line outro change fails the absent-package E2E and nothing else. - CLI unit 1243 passed, e2e 108 passed - typecheck 3/3, biome error-free
1 parent b719dc0 commit ac46a5a

5 files changed

Lines changed: 36 additions & 9 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'stash': patch
3+
---
4+
5+
`stash doctor` no longer reports "All checks passed." when `@cipherstash/stack`
6+
is absent. The package is an optional peer, so running `doctor` before `stash
7+
init` skips the encryption check entirely — the row already said so, but the
8+
outro claimed a pass for a check that never ran. It now ends with "stash doctor
9+
could not run every check.", the same line an unprobeable install gets, and
10+
still exits 0: an absent optional package is recoverable, not a failure.

‎packages/cli/src/commands/doctor/index.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface Probe {
4040

4141
const PROBES: Probe[] = [
4242
{
43-
label: 'Encryption engine (@cipherstash/stack → protect-ffi)',
43+
label: messages.doctor.encryptionProbeLabel,
4444
pkg: '@cipherstash/stack',
4545
subpath: './diagnostics',
4646
optional: true,
@@ -55,7 +55,7 @@ const PROBES: Probe[] = [
5555
},
5656
},
5757
{
58-
label: 'Auth (@cipherstash/auth)',
58+
label: messages.doctor.authProbeLabel,
5959
pkg: '@cipherstash/auth',
6060
async force() {
6161
// No counterpart call needed. This package's entry is `module.exports =
@@ -177,7 +177,12 @@ export async function doctorCommand(): Promise<void> {
177177
? messages.doctor.notInstalledOptional
178178
: messages.doctor.notInstalled,
179179
)
180-
if (!probe.optional) failed = true
180+
// The row stays green — absence before `stash init` is expected, and
181+
// the detail already says so — but the check did not RUN, which is the
182+
// same thing the too-old arm below records. Without this the outro said
183+
// every check passed while one of the two never executed.
184+
if (probe.optional) incomplete = true
185+
else failed = true
181186
} else if (isSubpathUnavailable(err, probe)) {
182187
report('warn', probe.label, messages.doctor.cannotProbe)
183188
incomplete = true

‎packages/cli/src/messages.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ export const messages = {
2626
title: 'stash doctor',
2727
/** Leader of the platform check line; the `<platform>-<arch>` is appended. */
2828
platformLabel: 'Platform',
29+
/**
30+
* Probe row labels. Here rather than inline in `commands/doctor/index.ts`
31+
* because both E2E suites match a row as `<label> — <detail>`: the detail
32+
* halves were already constants while the labels were copied into each
33+
* suite, so a copy tweak broke the tests from a file they don't name.
34+
*/
35+
encryptionProbeLabel:
36+
'Encryption engine (@cipherstash/stack → protect-ffi)',
37+
authProbeLabel: 'Auth (@cipherstash/auth)',
2938
allChecksPassed: 'All checks passed.',
3039
/** Row detail when a probe reached the loader and no platform binary was there. */
3140
nativeBinaryMissing: 'native binary missing',

‎packages/cli/tests/e2e/doctor-missing-binary.e2e.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ interface Target {
3535
const TARGETS: Target[] = [
3636
{
3737
pkg: '@cipherstash/protect-ffi',
38-
label: 'Encryption engine (@cipherstash/stack → protect-ffi)',
38+
label: messages.doctor.encryptionProbeLabel,
3939
// `src/load.cts`'s debug arm — a local cargo build sitting beside `lib/`.
4040
// A contributor who has run `pnpm run debug` has one, and without this the
4141
// fixture would load the binding it means to be missing.
4242
extra: String.raw`|(?:^|[\\/])index\.node$`,
4343
},
4444
{
4545
pkg: '@cipherstash/auth',
46-
label: 'Auth (@cipherstash/auth)',
46+
label: messages.doctor.authProbeLabel,
4747
// napi's local-build arm, tried before the platform package.
4848
extra: String.raw`|(?:^|[\\/])stack-auth-node\.node$`,
4949
},

‎packages/cli/tests/e2e/doctor-probe-classification.e2e.test.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ import { render } from '../helpers/pty.js'
2626
// the code and message pasted on. A fixture like that keeps passing when Node
2727
// changes either, which is the whole risk being covered.
2828

29-
/** Mirrors the probe labels in `src/commands/doctor/index.ts`. */
30-
const ENCRYPTION_LABEL = 'Encryption engine (@cipherstash/stack → protect-ffi)'
31-
const AUTH_LABEL = 'Auth (@cipherstash/auth)'
29+
const ENCRYPTION_LABEL = messages.doctor.encryptionProbeLabel
30+
const AUTH_LABEL = messages.doctor.authProbeLabel
3231

3332
interface Unresolve {
3433
/** The bare specifier to divert. */
@@ -125,7 +124,11 @@ describe.skipIf(!hooksAvailable)('stash doctor — probe classification', () =>
125124
expect(r.output).toContain(
126125
`${ENCRYPTION_LABEL} — ${messages.doctor.notInstalledOptional}`,
127126
)
128-
expect(r.output).toContain(messages.doctor.allChecksPassed)
127+
// Recoverable, so exit 0 and a green row — but the encryption check did not
128+
// run, and the outro must not say it passed. Same distinction the too-old
129+
// arm makes below; this arm predates it and claimed a pass.
130+
expect(r.output).toContain(messages.doctor.checksIncomplete)
131+
expect(r.output).not.toContain(messages.doctor.allChecksPassed)
129132
expect(r.output).not.toContain('Fatal error')
130133
// The optional package's absence must not be dressed up as a missing
131134
// binary — that would send the user to a reinstall for a package they

0 commit comments

Comments
 (0)