From c3fb35f4cac501bc4377afa5e83e8a65ea650817 Mon Sep 17 00:00:00 2001 From: jubnl Date: Mon, 13 Jul 2026 17:35:51 +0200 Subject: [PATCH] chore: remove test files --- plugin-sdk/src/cli/update-check-worker.ts | 52 ------ plugin-sdk/src/cli/update-check.ts | 112 ------------ plugin-sdk/test/update-check.test.ts | 209 ---------------------- 3 files changed, 373 deletions(-) delete mode 100644 plugin-sdk/src/cli/update-check-worker.ts delete mode 100644 plugin-sdk/src/cli/update-check.ts delete mode 100644 plugin-sdk/test/update-check.test.ts diff --git a/plugin-sdk/src/cli/update-check-worker.ts b/plugin-sdk/src/cli/update-check-worker.ts deleted file mode 100644 index 9f94523a4..000000000 --- a/plugin-sdk/src/cli/update-check-worker.ts +++ /dev/null @@ -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 { - 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 { - 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)); -} diff --git a/plugin-sdk/src/cli/update-check.ts b/plugin-sdk/src/cli/update-check.ts deleted file mode 100644 index b1a7b45e8..000000000 --- a/plugin-sdk/src/cli/update-check.ts +++ /dev/null @@ -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; - 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. - } -} diff --git a/plugin-sdk/test/update-check.test.ts b/plugin-sdk/test/update-check.test.ts deleted file mode 100644 index 345a95ac9..000000000 --- a/plugin-sdk/test/update-check.test.ts +++ /dev/null @@ -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()), - 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 { - 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]; - 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(); - }); -});