feat(status): show SecretBackend health in mcpctl status
Some checks failed
CI/CD / lint (pull_request) Successful in 1m3s
CI/CD / test (pull_request) Successful in 1m17s
CI/CD / typecheck (pull_request) Successful in 2m33s
CI/CD / smoke (pull_request) Failing after 1m52s
CI/CD / build (pull_request) Successful in 4m55s
CI/CD / publish (pull_request) Has been skipped

Adds a 'Secrets:' line to mcpctl status (and a secretBackends array to JSON
output) showing each backend healthy (✓) or, when tokenMeta.lastRotationError
is set (e.g. a dead OpenBao token), red ✗ with the error inline. Makes a
recurrence of BACKEND_TOKEN_DEAD visible at a glance instead of only in mcpd
logs. Verified live: 'Secrets: bao* ✓, default ✓'. +3 tests (28 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-06-16 23:13:50 +01:00
parent 4c7e648771
commit 467757b966
2 changed files with 118 additions and 2 deletions

View File

@@ -29,6 +29,7 @@ function baseDeps(overrides?: Partial<StatusCommandDeps>): Partial<StatusCommand
fetchProviders: async () => null,
fetchServerLlms: async () => null,
probeServerLlm: async () => ({ ok: true, ms: 12, say: 'hi' }),
fetchSecretBackends: async () => null,
isTTY: false,
...overrides,
};
@@ -45,6 +46,39 @@ afterEach(() => {
});
describe('status command', () => {
it('shows a healthy secret backend in the Secrets line', async () => {
const cmd = createStatusCommand(baseDeps({
fetchSecretBackends: async () => [
{ name: 'bao', type: 'openbao', isDefault: true, tokenMeta: { lastRotationError: null } },
{ name: 'default', type: 'plaintext' },
],
}));
await cmd.parseAsync([], { from: 'user' });
const out = output.join('\n');
expect(out).toContain('Secrets:');
expect(out).toContain('bao* ✓');
expect(out).toContain('default ✓');
});
it('flags a dead secret-backend token in the Secrets line', async () => {
const cmd = createStatusCommand(baseDeps({
fetchSecretBackends: async () => [
{ name: 'bao', type: 'openbao', isDefault: true, tokenMeta: { lastRotationError: 'BACKEND_TOKEN_DEAD: rejected the stored token\nmore detail' } },
],
}));
await cmd.parseAsync([], { from: 'user' });
const out = output.join('\n');
expect(out).toContain('bao* ✗');
expect(out).toContain('BACKEND_TOKEN_DEAD');
expect(out).not.toContain('more detail'); // only first line, truncated
});
it('omits the Secrets line when mcpd returns no backends', async () => {
const cmd = createStatusCommand(baseDeps({ fetchSecretBackends: async () => null }));
await cmd.parseAsync([], { from: 'user' });
expect(output.join('\n')).not.toContain('Secrets:');
});
it('shows status in table format', async () => {
const cmd = createStatusCommand(baseDeps());
await cmd.parseAsync([], { from: 'user' });