mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
* feat(cli): show cached update notices after commands * docs(cli): describe update notices * fix(cli): type update check environment * fix(cli): decouple update notice display from refresh and harden suppression Display a cached "update available" notice based solely on the lastNoticeAt 24h throttle, independent of checkedAt refresh freshness, matching the design's independent display/refresh decisions. Suppress the check unconditionally under --json, CI, and non-TTY before consulting output-mode preferences, so a KTX_OUTPUT=pretty override can no longer make CI/non-TTY contexts phone npm.
80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const requestMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('node:https', () => ({
|
|
request: requestMock,
|
|
}));
|
|
|
|
type MockResponse = EventEmitter & { statusCode?: number };
|
|
type MockRequest = EventEmitter & {
|
|
destroy: ReturnType<typeof vi.fn>;
|
|
end: () => void;
|
|
setTimeout: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
function mockHttpsResponse(statusCode: number, body: string): { socket: { unref: ReturnType<typeof vi.fn> } } {
|
|
const socket = { unref: vi.fn() };
|
|
requestMock.mockImplementation((_url: unknown, _options: unknown, callback: (response: MockResponse) => void) => {
|
|
const request = new EventEmitter() as MockRequest;
|
|
request.destroy = vi.fn();
|
|
request.setTimeout = vi.fn();
|
|
request.end = () => {
|
|
request.emit('socket', socket);
|
|
const response = new EventEmitter() as MockResponse;
|
|
response.statusCode = statusCode;
|
|
callback(response);
|
|
response.emit('data', Buffer.from(body));
|
|
response.emit('end');
|
|
};
|
|
return request;
|
|
});
|
|
return { socket };
|
|
}
|
|
|
|
describe('fetchDistTags', () => {
|
|
beforeEach(() => {
|
|
requestMock.mockReset();
|
|
});
|
|
|
|
it('fetches @kaelio/ktx npm dist-tags and unrefs the socket', async () => {
|
|
const { socket } = mockHttpsResponse(200, JSON.stringify({ latest: '0.10.0', next: '0.11.0-rc.1' }));
|
|
const { fetchDistTags } = await import('../../src/update-check/registry.js');
|
|
|
|
await expect(fetchDistTags()).resolves.toEqual({ latest: '0.10.0', next: '0.11.0-rc.1' });
|
|
|
|
expect(requestMock).toHaveBeenCalledWith(
|
|
expect.any(URL),
|
|
expect.objectContaining({
|
|
method: 'GET',
|
|
headers: expect.objectContaining({ accept: 'application/json' }),
|
|
}),
|
|
expect.any(Function),
|
|
);
|
|
const [url] = requestMock.mock.calls[0] as [URL];
|
|
expect(url.toString()).toBe('https://registry.npmjs.org/-/package/@kaelio/ktx/dist-tags');
|
|
expect(socket.unref).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('rejects non-2xx responses', async () => {
|
|
mockHttpsResponse(503, 'registry unavailable');
|
|
const { fetchDistTags } = await import('../../src/update-check/registry.js');
|
|
|
|
await expect(fetchDistTags()).rejects.toThrow('npm dist-tags request failed with 503');
|
|
});
|
|
|
|
it('rejects invalid JSON payloads', async () => {
|
|
mockHttpsResponse(200, '{bad json');
|
|
const { fetchDistTags } = await import('../../src/update-check/registry.js');
|
|
|
|
await expect(fetchDistTags()).rejects.toThrow();
|
|
});
|
|
|
|
it('rejects payloads that are not string dist-tag maps', async () => {
|
|
mockHttpsResponse(200, JSON.stringify({ latest: 123 }));
|
|
const { fetchDistTags } = await import('../../src/update-check/registry.js');
|
|
|
|
await expect(fetchDistTags()).rejects.toThrow();
|
|
});
|
|
});
|