mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 03:12:32 +00:00
The repo publishes zero GitHub releases, so releases/latest is a permanent 404 and fetchLatestRelease() could never succeed — the entire upgrade notification subsystem was a silent no-op, and refreshUpdateCache() cached a fabricated up_to_date marker on every failure. - Resolve the latest version from raw.githubusercontent.com/.../master/VERSION (same trusted host fetchChangelog already uses). Bounded, shape-gated parse; handles legacy 3-segment and -suffix channel forms. - Discriminate network_error from no_releases in the --json error field and human output. - Never write up_to_date on a failed check: preserve the last-known-good marker (mtime bump keeps the TTL throttle) or write nothing. - Rejected the issue's proposed npm fallback: the gbrain npm package is an unrelated GPU library (#505) and would produce false upgrade prompts. Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
204 lines
6.0 KiB
TypeScript
204 lines
6.0 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
parseSemver,
|
|
isMinorOrMajorBump,
|
|
isNewerVersion,
|
|
extractChangelogBetween,
|
|
} from '../src/commands/check-update.ts';
|
|
|
|
describe('parseSemver', () => {
|
|
test('parses standard version', () => {
|
|
expect(parseSemver('0.4.0')).toEqual([0, 4, 0, 0]);
|
|
});
|
|
|
|
test('strips v prefix', () => {
|
|
expect(parseSemver('v0.5.0')).toEqual([0, 5, 0, 0]);
|
|
});
|
|
|
|
test('returns null for malformed version', () => {
|
|
expect(parseSemver('0.4')).toBeNull();
|
|
expect(parseSemver('abc')).toBeNull();
|
|
expect(parseSemver('')).toBeNull();
|
|
});
|
|
|
|
test('preserves the 4th micro segment', () => {
|
|
expect(parseSemver('0.2.0.1')).toEqual([0, 2, 0, 1]);
|
|
});
|
|
});
|
|
|
|
describe('isNewerVersion', () => {
|
|
test('detects the patch segment used by the gbrain release train', () => {
|
|
expect(isNewerVersion('0.42.10.0', '0.42.66.0')).toBe(true);
|
|
});
|
|
|
|
test('detects a micro follow-up release', () => {
|
|
expect(isNewerVersion('0.42.66.0', '0.42.66.1')).toBe(true);
|
|
});
|
|
|
|
test('orders legacy 3-segment against 4-segment: 0.42.67.0 > 0.42.66.1 > 0.42.66', () => {
|
|
expect(isNewerVersion('0.42.66.1', '0.42.67.0')).toBe(true);
|
|
expect(isNewerVersion('0.42.66', '0.42.66.1')).toBe(true);
|
|
expect(isNewerVersion('0.42.66', '0.42.66.0')).toBe(false); // 3-segment == its .0 micro
|
|
expect(isNewerVersion('0.42.67.0', '0.42.66.1')).toBe(false);
|
|
});
|
|
|
|
test('rejects equal, older, and malformed versions', () => {
|
|
expect(isNewerVersion('0.42.66.0', '0.42.66.0')).toBe(false);
|
|
expect(isNewerVersion('0.42.66.1', '0.42.66.0')).toBe(false);
|
|
expect(isNewerVersion('0.42.66.0', 'garbage')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isMinorOrMajorBump', () => {
|
|
test('0.4.0 vs 0.5.0 → update available (minor bump)', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', '0.5.0')).toBe(true);
|
|
});
|
|
|
|
test('0.4.0 vs 0.4.1 → NOT available (patch only)', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', '0.4.1')).toBe(false);
|
|
});
|
|
|
|
test('0.4.0 vs 1.0.0 → update available (major bump)', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', '1.0.0')).toBe(true);
|
|
});
|
|
|
|
test('0.4.0 vs 0.4.0 → NOT available (same version)', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', '0.4.0')).toBe(false);
|
|
});
|
|
|
|
test('0.4.0 vs 0.3.0 → NOT available (older)', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', '0.3.0')).toBe(false);
|
|
});
|
|
|
|
test('0.4.1 vs 0.5.0 → update available (minor bump, different patch)', () => {
|
|
expect(isMinorOrMajorBump('0.4.1', '0.5.0')).toBe(true);
|
|
});
|
|
|
|
test('malformed version → returns false', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', 'abc')).toBe(false);
|
|
expect(isMinorOrMajorBump('bad', '0.5.0')).toBe(false);
|
|
});
|
|
|
|
test('handles v prefix on latest', () => {
|
|
expect(isMinorOrMajorBump('0.4.0', 'v0.5.0')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('extractChangelogBetween', () => {
|
|
const changelog = `# Changelog
|
|
|
|
## [0.5.0] - 2026-05-01
|
|
|
|
### Added
|
|
- Feature X
|
|
|
|
## [0.4.1] - 2026-04-15
|
|
|
|
### Fixed
|
|
- Bug Y
|
|
|
|
## [0.4.0] - 2026-04-09
|
|
|
|
### Added
|
|
- Feature Z
|
|
|
|
## [0.3.0] - 2026-04-08
|
|
|
|
### Added
|
|
- Feature W
|
|
`;
|
|
|
|
test('extracts entries between 0.4.0 and 0.5.0', () => {
|
|
const result = extractChangelogBetween(changelog, '0.4.0', '0.5.0');
|
|
expect(result).toContain('Feature X');
|
|
expect(result).toContain('Bug Y');
|
|
expect(result).not.toContain('Feature Z');
|
|
expect(result).not.toContain('Feature W');
|
|
});
|
|
|
|
test('extracts only 0.5.0 when upgrading from 0.4.1', () => {
|
|
const result = extractChangelogBetween(changelog, '0.4.1', '0.5.0');
|
|
expect(result).toContain('Feature X');
|
|
expect(result).not.toContain('Bug Y');
|
|
});
|
|
|
|
test('returns empty for same version', () => {
|
|
const result = extractChangelogBetween(changelog, '0.5.0', '0.5.0');
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
test('returns empty for malformed from version', () => {
|
|
const result = extractChangelogBetween(changelog, 'bad', '0.5.0');
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
test('does not capture older major versions incorrectly', () => {
|
|
const crossMajor = `# Changelog
|
|
|
|
## [2.0.0] - 2026-06-01
|
|
### Added
|
|
- Major 2
|
|
|
|
## [0.5.0] - 2026-05-01
|
|
### Added
|
|
- Minor 5
|
|
`;
|
|
const result = extractChangelogBetween(crossMajor, '1.2.0', '2.0.0');
|
|
expect(result).toContain('Major 2');
|
|
expect(result).not.toContain('Minor 5');
|
|
});
|
|
|
|
test('distinguishes micro releases', () => {
|
|
const micro = `# Changelog
|
|
|
|
## [0.42.66.1] - 2026-07-26
|
|
- Follow-up fix
|
|
|
|
## [0.42.66.0] - 2026-07-25
|
|
- Original release
|
|
`;
|
|
const result = extractChangelogBetween(micro, '0.42.66.0', '0.42.66.1');
|
|
expect(result).toContain('Follow-up fix');
|
|
expect(result).not.toContain('Original release');
|
|
});
|
|
});
|
|
|
|
describe('check-update CLI', () => {
|
|
test('check-update is in CLI_ONLY set', async () => {
|
|
const source = await Bun.file(
|
|
new URL('../src/cli.ts', import.meta.url).pathname
|
|
).text();
|
|
expect(source).toContain("'check-update'");
|
|
});
|
|
|
|
test('--help prints usage and exits 0', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'check-update', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('check-update');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('--json returns valid JSON with required fields', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'check-update', '--json'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(exitCode).toBe(0);
|
|
|
|
const output = JSON.parse(stdout);
|
|
expect(output).toHaveProperty('current_version');
|
|
expect(output).toHaveProperty('update_available');
|
|
expect(output).toHaveProperty('upgrade_command');
|
|
expect(output).toHaveProperty('current_source', 'package-json');
|
|
expect(typeof output.update_available).toBe('boolean');
|
|
});
|
|
});
|