ktx/packages/cli/test/update-check/cache.test.ts
Andrey Avtomonov 698efdcef8
feat(cli): add channel-aware update notifier (#265)
* 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.
2026-06-06 10:42:10 +02:00

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);
});
});