* feat: add `gbrain check-update` command for auto-update notifications Deterministic collector that checks GitHub Releases for new versions, compares semver (minor+ only, skips patches), and fetches changelog diffs. Exports `detectInstallMethod()` from upgrade.ts for reuse. Includes 15 unit tests covering version comparison, CLI wiring, and error handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add E2E upgrade tests against real GitHub API Exercises check-update CLI end-to-end: valid JSON output, human-readable mode, help text, graceful no-releases handling, and version comparison wiring. Skips gracefully when network is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add SKILLPACK Section 17 — auto-update notifications Full agent playbook for the update lifecycle: check, notify, consent, upgrade, skills refresh, schema sync, report. Includes standalone self-update for skillpack-only users via version markers and raw GitHub URL fetching. Adds version markers to both SKILLPACK and RECOMMENDED_SCHEMA headers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add auto-update step 7 to install paste, setup Phase G, migrations dir Adds step 7 to the OpenClaw install paste (default-on update checks). Setup skill gets Phase G (conditional offer for manual installs) and schema state tracking via ~/.gbrain/update-state.json. Creates skills/migrations/ directory for version-specific upgrade directives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update CLAUDE.md with E2E test DB lifecycle, migration conventions Adds E2E test DB lifecycle instructions (spin up, run, tear down). Documents version migration convention (skills/migrations/v[version].md) and schema state tracking (~/.gbrain/update-state.json). Updates test file counts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: broken semver comparison in extractChangelogBetween The version range check compared minor versions without guarding on major being equal, causing incorrect changelog entries to be captured (e.g., v0.5.0 would match when upgrading from v1.2.0). Extracted semverGt/semverLte helpers for correct comparisons. Added 5 tests for extractChangelogBetween covering cross-major, same-version, and malformed input cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.4.1) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { parseSemver, isMinorOrMajorBump, extractChangelogBetween } from '../src/commands/check-update.ts';
|
|
|
|
describe('parseSemver', () => {
|
|
test('parses standard version', () => {
|
|
expect(parseSemver('0.4.0')).toEqual([0, 4, 0]);
|
|
});
|
|
|
|
test('strips v prefix', () => {
|
|
expect(parseSemver('v0.5.0')).toEqual([0, 5, 0]);
|
|
});
|
|
|
|
test('returns null for malformed version', () => {
|
|
expect(parseSemver('0.4')).toBeNull();
|
|
expect(parseSemver('abc')).toBeNull();
|
|
expect(parseSemver('')).toBeNull();
|
|
});
|
|
|
|
test('handles 4-part versions (takes first 3)', () => {
|
|
expect(parseSemver('0.2.0.1')).toEqual([0, 2, 0]);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|