mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +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.
95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
readUpdateCheckCache,
|
|
updateCheckCachePath,
|
|
writeUpdateCheckCache,
|
|
} from '../../src/update-check/cache.js';
|
|
|
|
describe('update-check cache', () => {
|
|
let homeDir: string;
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(join(tmpdir(), 'ktx-update-check-cache-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(homeDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('uses ~/.ktx/update-check.json', () => {
|
|
expect(updateCheckCachePath(homeDir)).toBe(join(homeDir, '.ktx', 'update-check.json'));
|
|
});
|
|
|
|
it('round-trips strict cache data', async () => {
|
|
await writeUpdateCheckCache(
|
|
{
|
|
checkedAt: '2026-06-06T10:00:00.000Z',
|
|
channel: 'latest',
|
|
installedVersion: '0.9.0',
|
|
latestForChannel: '0.10.0',
|
|
lastNoticeAt: '2026-06-06T11:00:00.000Z',
|
|
},
|
|
{ homeDir },
|
|
);
|
|
|
|
await expect(readUpdateCheckCache({ homeDir })).resolves.toEqual({
|
|
checkedAt: '2026-06-06T10:00:00.000Z',
|
|
channel: 'latest',
|
|
installedVersion: '0.9.0',
|
|
latestForChannel: '0.10.0',
|
|
lastNoticeAt: '2026-06-06T11:00:00.000Z',
|
|
});
|
|
});
|
|
|
|
it('returns null when the cache file is missing', async () => {
|
|
await expect(readUpdateCheckCache({ homeDir })).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null when the cache file is corrupt JSON', async () => {
|
|
await mkdir(join(homeDir, '.ktx'), { recursive: true });
|
|
await writeFile(updateCheckCachePath(homeDir), '{bad json', 'utf-8');
|
|
|
|
await expect(readUpdateCheckCache({ homeDir })).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null when the cache has unknown fields', async () => {
|
|
await mkdir(join(homeDir, '.ktx'), { recursive: true });
|
|
await writeFile(
|
|
updateCheckCachePath(homeDir),
|
|
JSON.stringify(
|
|
{
|
|
checkedAt: '2026-06-06T10:00:00.000Z',
|
|
channel: 'latest',
|
|
installedVersion: '0.9.0',
|
|
latestForChannel: '0.10.0',
|
|
unexpected: true,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf-8',
|
|
);
|
|
|
|
await expect(readUpdateCheckCache({ homeDir })).resolves.toBeNull();
|
|
});
|
|
|
|
it('writes formatted JSON with a trailing newline', async () => {
|
|
await writeUpdateCheckCache(
|
|
{
|
|
checkedAt: '2026-06-06T10:00:00.000Z',
|
|
channel: 'next',
|
|
installedVersion: '0.10.0-rc.1',
|
|
latestForChannel: '0.10.0-rc.2',
|
|
},
|
|
{ homeDir },
|
|
);
|
|
|
|
const raw = await readFile(updateCheckCachePath(homeDir), 'utf-8');
|
|
expect(raw).toContain('"channel": "next"');
|
|
expect(raw.endsWith('\n')).toBe(true);
|
|
});
|
|
});
|