mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
chore: remove test files
This commit is contained in:
@@ -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));
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user