chore: remove test files

This commit is contained in:
jubnl
2026-07-13 17:35:51 +02:00
parent 5003dc7f33
commit c3fb35f4ca
3 changed files with 0 additions and 373 deletions
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env node
/**
* The background half of the update notice (#plugins) — see update-check.ts.
*
* Spawned detached and unref'd, so nothing is waiting on it: it asks npm for the
* `latest` dist-tag, writes the cache the NEXT command will read, and exits. It
* prints nothing, ever, and every failure (offline, DNS, 404, timeout, read-only
* cache dir) is a silent no-op — the author is not debugging our update check.
*/
import fs from 'node:fs';
import path from 'node:path';
import { PACKAGE_NAME, cachePath, type UpdateCache } from './update-check.js';
const REGISTRY = 'https://registry.npmjs.org';
/** Generous: nobody is waiting on this process. */
const TIMEOUT_MS = 3000;
/** Ask the registry for the `latest` dist-tag. `null` on any failure. */
export async function fetchLatest(registry: string = REGISTRY): Promise<string | null> {
try {
const res = await fetch(`${registry}/${PACKAGE_NAME}/latest`, {
signal: AbortSignal.timeout(TIMEOUT_MS),
headers: { accept: 'application/json' },
});
if (!res.ok) return null;
const body = (await res.json()) as { version?: unknown };
return typeof body?.version === 'string' ? body.version : null;
} catch {
return null;
}
}
/** Write the cache the next command reads. Silently gives up if it can't. */
export function writeCache(file: string, cache: UpdateCache): void {
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(cache));
} catch {
// A cache we can't write just means we check again next time.
}
}
/** Fetch → cache. Exported whole so the tests can drive it with a stubbed `fetch`. */
export async function refresh(file: string = cachePath(), now: number = Date.now()): Promise<void> {
const latest = await fetchLatest();
if (latest) writeCache(file, { lastCheck: now, latest });
}
// Bin entry. Never throws, never prints, always exits 0.
if (process.argv[1] && process.argv[1].endsWith('update-check-worker.js')) {
refresh().catch(() => {}).finally(() => process.exit(0));
}
-112
View File
@@ -1,112 +0,0 @@
/**
* "Your SDK is out of date" — the advisory update notice (#plugins).
*
* A stale SDK is not a cosmetic problem: the registry-entry format, the manifest
* validator and the permission catalog all move with the host, so an author on an
* old SDK can happily `pack` and `submit` an entry that today's registry CI rejects.
*
* Three rules keep this safe to call from the top of every command:
* 1. ZERO added latency. The command only ever prints from a cache file; the
* refresh runs in a DETACHED child (see update-check-worker.ts). An un-awaited
* in-process fetch() would NOT do — Node keeps the socket alive and won't exit
* until it resolves, so every command would pay ~300ms.
* 2. STDERR only. stdout is a pure data channel (`entry`, `pack --json`, PR URLs) —
* see the contract in ui.ts.
* 3. It CANNOT fail a command. Every path is wrapped; a broken cache, a missing
* worker or an offline machine are all silently indistinguishable from "no news".
*/
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import semver from 'semver';
export const PACKAGE_NAME = 'trek-plugin-sdk';
export const CACHE_FILE = 'update-check.json';
export const TTL_MS = 24 * 60 * 60 * 1000;
export interface UpdateCache {
/** Epoch ms of the last successful registry read. */
lastCheck: number;
/** The `latest` dist-tag as of `lastCheck`. */
latest: string;
}
/** This package's own version — the thing we compare the registry against. */
export function sdkVersion(): string {
try {
const pkg = createRequire(import.meta.url)('../../package.json') as { version?: string };
return pkg.version ?? '0.0.0';
} catch {
return '0.0.0';
}
}
/**
* Where the cache lives. Takes its inputs as arguments (rather than reading the
* environment itself) so tests can point it at a temp dir without touching $HOME.
*/
export function cachePath(env: NodeJS.ProcessEnv = process.env, homedir = os.homedir(), tmpdir = os.tmpdir()): string {
const base = env.XDG_CACHE_HOME || (homedir ? path.join(homedir, '.cache') : tmpdir);
return path.join(base, PACKAGE_NAME, CACHE_FILE);
}
/** Missing, unreadable, corrupt, or wrong-shaped — all are simply "no news". Never throws. */
export function readCache(file: string): UpdateCache | null {
try {
const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Partial<UpdateCache>;
if (typeof raw?.lastCheck !== 'number' || typeof raw?.latest !== 'string') return null;
return { lastCheck: raw.lastCheck, latest: raw.latest };
} catch {
return null;
}
}
/** An absent cache is stale — that is what triggers the very first refresh. */
export function isStale(cache: UpdateCache | null, now: number, ttlMs: number = TTL_MS): boolean {
return !cache || now - cache.lastCheck >= ttlMs;
}
/**
* The notice is for humans at a terminal, and for nobody else: not CI logs, not
* pipes, and not authors who have opted out.
*/
export function isSilenced(env: NodeJS.ProcessEnv = process.env, isTTY: boolean = Boolean(process.stderr.isTTY)): boolean {
if (env.TREK_SDK_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI) return true;
return !isTTY;
}
/** The notice itself — `null` when there is nothing to say (same version, or ahead of it). */
export function banner(current: string, latest: string): string | null {
if (!semver.valid(current) || !semver.valid(latest) || !semver.gt(latest, current)) return null;
const dim = (s: string): string => `\x1b[2m${s}\x1b[0m`;
return dim(`\n Update available ${current} → ${latest} · npm i -g ${PACKAGE_NAME}@latest\n`);
}
/** Detached + unref'd, so the parent process exits the instant its own work is done. */
function spawnRefresh(): void {
const worker = fileURLToPath(new URL('./update-check-worker.js', import.meta.url));
spawn(process.execPath, [worker], { detached: true, stdio: 'ignore' }).unref();
}
/**
* The one entry point. Call it at the top of a command; it returns immediately.
* Prints from cache, and refreshes that cache in the background for the NEXT run —
* which is why a brand-new install learns about an update one run late. That is the
* price of never making an author wait on the network.
*/
export function maybeNotify(now: number = Date.now()): void {
try {
if (isSilenced()) return;
const cache = readCache(cachePath());
if (cache) {
const msg = banner(sdkVersion(), cache.latest);
if (msg) process.stderr.write(msg);
}
if (isStale(cache, now)) spawnRefresh();
} catch {
// An update notice is never worth failing a command over.
}
}
-209
View File
@@ -1,209 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const spawn = vi.hoisted(() => vi.fn(() => ({ unref: vi.fn() })));
vi.mock('node:child_process', async (orig) => ({
...(await orig<typeof import('node:child_process')>()),
spawn,
}));
const {
banner, cachePath, readCache, isStale, isSilenced, maybeNotify, sdkVersion, PACKAGE_NAME, TTL_MS,
} = await import('../src/cli/update-check.js');
const { fetchLatest, writeCache, refresh } = await import('../src/cli/update-check-worker.js');
let tmp: string;
beforeEach(() => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'update-check-'));
spawn.mockClear();
});
afterEach(() => {
fs.rmSync(tmp, { recursive: true, force: true });
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
describe('banner', () => {
it('announces only a strictly newer version', () => {
expect(banner('1.2.0', '1.5.0')).toContain('1.2.0 → 1.5.0');
expect(banner('1.5.0', '1.5.0')).toBeNull();
expect(banner('1.6.0', '1.5.0')).toBeNull();
});
it('says nothing when either version is unparseable', () => {
expect(banner('not-a-version', '1.5.0')).toBeNull();
expect(banner('1.2.0', 'garbage')).toBeNull();
});
});
describe('sdkVersion', () => {
it("reads this package's real version", () => {
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
expect(sdkVersion()).toBe(pkg.version);
});
});
describe('cachePath', () => {
it('prefers XDG_CACHE_HOME, else ~/.cache', () => {
expect(cachePath({ XDG_CACHE_HOME: '/xdg' }, '/home/a', '/tmp'))
.toBe(path.join('/xdg', PACKAGE_NAME, 'update-check.json'));
expect(cachePath({}, '/home/a', '/tmp'))
.toBe(path.join('/home/a', '.cache', PACKAGE_NAME, 'update-check.json'));
});
});
describe('readCache', () => {
it('reads a well-formed cache', () => {
const f = path.join(tmp, 'c.json');
fs.writeFileSync(f, JSON.stringify({ lastCheck: 42, latest: '9.9.9' }));
expect(readCache(f)).toEqual({ lastCheck: 42, latest: '9.9.9' });
});
it('returns null — never throws — for missing, corrupt or wrong-shaped files', () => {
expect(readCache(path.join(tmp, 'nope.json'))).toBeNull();
const corrupt = path.join(tmp, 'corrupt.json');
fs.writeFileSync(corrupt, '{not json');
expect(readCache(corrupt)).toBeNull();
const wrong = path.join(tmp, 'wrong.json');
fs.writeFileSync(wrong, JSON.stringify({ latest: 5 }));
expect(readCache(wrong)).toBeNull();
});
});
describe('isStale', () => {
it('treats an absent cache as stale, and expires at the TTL boundary', () => {
expect(isStale(null, 1_000_000)).toBe(true);
expect(isStale({ lastCheck: 1000, latest: '1.0.0' }, 1000 + TTL_MS - 1)).toBe(false);
expect(isStale({ lastCheck: 1000, latest: '1.0.0' }, 1000 + TTL_MS)).toBe(true);
});
});
describe('isSilenced', () => {
it('is silent in CI, when opted out, and when stderr is not a TTY', () => {
expect(isSilenced({ CI: '1' }, true)).toBe(true);
expect(isSilenced({ NO_UPDATE_NOTIFIER: '1' }, true)).toBe(true);
expect(isSilenced({ TREK_SDK_NO_UPDATE_CHECK: '1' }, true)).toBe(true);
expect(isSilenced({}, false)).toBe(true);
expect(isSilenced({}, true)).toBe(false);
});
});
describe('maybeNotify', () => {
/** A human at a terminal, with $XDG_CACHE_HOME pointed at the temp dir. */
function asHuman(): ReturnType<typeof vi.spyOn> {
vi.stubEnv('CI', '');
vi.stubEnv('NO_UPDATE_NOTIFIER', '');
vi.stubEnv('TREK_SDK_NO_UPDATE_CHECK', '');
vi.stubEnv('XDG_CACHE_HOME', tmp);
vi.spyOn(process.stderr, 'isTTY', 'get').mockReturnValue(true);
return vi.spyOn(process.stderr, 'write').mockReturnValue(true);
}
function seedCache(cache: { lastCheck: number; latest: string }): void {
const f = path.join(tmp, PACKAGE_NAME, 'update-check.json');
fs.mkdirSync(path.dirname(f), { recursive: true });
fs.writeFileSync(f, JSON.stringify(cache));
}
it('prints the notice when a fresh cache holds a newer version', () => {
const write = asHuman();
seedCache({ lastCheck: Date.now(), latest: '999.0.0' });
maybeNotify();
expect(write).toHaveBeenCalledOnce();
expect(String(write.mock.calls[0][0])).toContain(`999.0.0 · npm i -g ${PACKAGE_NAME}@latest`);
expect(spawn).not.toHaveBeenCalled(); // fresh cache — no refresh
});
it('prints nothing when the cached version is the one we are running', () => {
const write = asHuman();
seedCache({ lastCheck: Date.now(), latest: sdkVersion() });
maybeNotify();
expect(write).not.toHaveBeenCalled();
});
it('is silent on a cold cache, but spawns the background refresh for next time', () => {
const write = asHuman();
maybeNotify();
expect(write).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledOnce();
const [, args, opts] = spawn.mock.calls[0] as unknown as [string, string[], Record<string, unknown>];
expect(args[0]).toMatch(/update-check-worker\.js$/);
expect(opts).toMatchObject({ detached: true, stdio: 'ignore' });
});
it('refreshes a stale cache while still printing what it knows', () => {
const write = asHuman();
seedCache({ lastCheck: Date.now() - TTL_MS - 1, latest: '999.0.0' });
maybeNotify();
expect(write).toHaveBeenCalledOnce();
expect(spawn).toHaveBeenCalledOnce();
});
it('does nothing at all — no print, no spawn — when silenced', () => {
const write = asHuman();
vi.stubEnv('CI', '1');
seedCache({ lastCheck: Date.now(), latest: '999.0.0' });
maybeNotify();
expect(write).not.toHaveBeenCalled();
expect(spawn).not.toHaveBeenCalled();
});
it('survives a corrupt cache without throwing', () => {
const write = asHuman();
const f = path.join(tmp, PACKAGE_NAME, 'update-check.json');
fs.mkdirSync(path.dirname(f), { recursive: true });
fs.writeFileSync(f, 'not json at all');
expect(() => maybeNotify()).not.toThrow();
expect(write).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledOnce(); // unreadable == no news == refresh
});
});
describe('worker', () => {
it('returns the latest dist-tag version', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.0' }))));
await expect(fetchLatest()).resolves.toBe('2.0.0');
});
it('returns null on a non-200, a malformed body, or a network failure', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response('nope', { status: 404 })));
await expect(fetchLatest()).resolves.toBeNull();
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: 42 }))));
await expect(fetchLatest()).resolves.toBeNull();
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('ENOTFOUND'); }));
await expect(fetchLatest()).resolves.toBeNull();
});
it('writes the cache only when the fetch succeeded', async () => {
const f = path.join(tmp, 'sub', 'update-check.json');
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline'); }));
await refresh(f, 123);
expect(fs.existsSync(f)).toBe(false);
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '3.1.0' }))));
await refresh(f, 123);
expect(readCache(f)).toEqual({ lastCheck: 123, latest: '3.1.0' });
});
it('gives up silently when the cache is unwritable', () => {
expect(() => writeCache('/proc/definitely/not/writable.json', { lastCheck: 1, latest: '1.0.0' })).not.toThrow();
});
});