GitNexus/gitnexus/test/unit/cli-message.test.ts

111 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Unit tests for `gitnexus/src/cli/cli-message.ts`.
*
* cli-message is the helper for user-facing CLI banners and error guidance.
* The contract: each call writes plain text to stderr AND emits a
* structured pino record through the singleton logger.
*
* Tests verify both halves of the tee, plus shape contracts (newline
* handling, structured fields, tee survival across messages with
* embedded newlines).
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { cliInfo, cliWarn, cliError, cliInfoKey } from '../../src/cli/cli-message.js';
import { setCliLanguage } from '../../src/cli/i18n/index.js';
import { _captureLogger, type LoggerCapture } from '../../src/core/logger.js';
describe('cli-message — stderr + logger tee', () => {
let cap: LoggerCapture;
let stderrSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
cap = _captureLogger();
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
setCliLanguage(null);
stderrSpy.mockRestore();
cap.restore();
});
it('cliInfo writes plain text to stderr and emits a structured info record', () => {
cliInfo('hello');
// Plain stderr write
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
expect(stderrCalls).toContain('hello\n');
// Structured logger record
const records = cap.records();
expect(records.some((r) => r.msg === 'hello' && r.level === 30)).toBe(true);
});
it('cliWarn writes to stderr and emits at warn level (40)', () => {
cliWarn('caution');
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
expect(stderrCalls).toContain('caution\n');
const records = cap.records();
expect(records.some((r) => r.msg === 'caution' && r.level === 40)).toBe(true);
});
it('cliError writes to stderr and emits at error level (50) with structured fields', () => {
cliError('boom', { code: 'EADDRINUSE', port: 4747 });
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
expect(stderrCalls).toContain('boom\n');
const records = cap.records();
const errorRecord = records.find((r) => r.msg === 'boom' && r.level === 50);
expect(errorRecord).toBeDefined();
expect(errorRecord?.code).toBe('EADDRINUSE');
expect(errorRecord?.port).toBe(4747);
});
it('does not double-newline an already-newlined message', () => {
cliInfo('already-terminated\n');
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
// Exactly one trailing \n, not two.
expect(stderrCalls).toContain('already-terminated\n');
expect(stderrCalls.includes('already-terminated\n\n')).toBe(false);
});
it('preserves embedded newlines in multi-line messages (does not split into multiple records)', () => {
cliError('line one\nline two\nline three');
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
// The whole multi-line block goes to stderr in one write, with a
// trailing newline appended.
expect(stderrCalls).toContain('line one\nline two\nline three\n');
// The structured record carries the full message as a single field.
const records = cap.records();
expect(records.some((r) => r.msg === 'line one\nline two\nline three' && r.level === 50)).toBe(
true,
);
});
it('cliInfoKey translates key-based messages before teeing', () => {
setCliLanguage('zh-CN');
cliInfoKey('list.title', { count: 3 });
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
expect(stderrCalls).toContain('已索引仓库3\n');
expect(cap.records().some((r) => r.msg === '已索引仓库3' && r.level === 30)).toBe(true);
});
it('handles an empty message — stderr gets a bare newline, logger gets msg:""', () => {
cliInfo('');
const stderrCalls = stderrSpy.mock.calls.map(([chunk]) =>
typeof chunk === 'string' ? chunk : chunk.toString(),
);
expect(stderrCalls).toContain('\n');
const records = cap.records();
expect(records.some((r) => r.msg === '' && r.level === 30)).toBe(true);
});
});