mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(self-upgrade): decision/cache/snooze foundation + atomic binary self-update Pure decideSelfUpgrade (invocation + autopilot channels), atomic untrusted cache + escalating snooze + shared marker grammar (forged-marker rejection), semver helpers, and real darwin-arm64/linux-x64 binary self-update (download -> fsync -> smoke -> atomic rename; failure leaves old binary intact). Tests incl. real-HTTP-server swap E2E. * feat(self-upgrade): check-update cache/markers, self-upgrade command, CLI heartbeat hook check-update gains gstack-style cache/snooze/markers + refreshUpdateCache + exported fetchLatestRelease. New 'gbrain self-upgrade' command. cli.ts emits the update marker on every invocation (cache-read-only hot path, detached single-flight refresh, skip-set + recursion guard + NODE_ENV=test gate). * feat(self-upgrade): autopilot silent channel, doctor check, runPostUpgrade setup, config + identity marker autopilot opt-in silent channel (auto+quiet+idle, swap-only+breadcrumb+exit-relaunch) + installSystemd Restart=always + migrateSystemdUnitToRestartAlways. doctor self_upgrade_health. runPostUpgrade applySelfUpgradeSetup (one-time consent + systemd rewrite). init defaults mode=notify. config self_upgrade plane + KNOWN_CONFIG_KEYS. get_brain_identity carries update marker. * docs(self-upgrade): gbrain-upgrade agent skill, RESOLVER/manifest, auto-update doc reversal, HEARTBEAT New skills/gbrain-upgrade agent flow (mirror gstack-upgrade) wired into RESOLVER + manifest. upgrades-auto-update.md reversed to document opt-in auto + conservative gates. HEARTBEAT self-upgrade --check-only line. llms-full regenerated. * chore: bump version and changelog (v0.42.12.0) Self-upgrading gbrain: invocation-riding update marker + opt-in autopilot silent channel + real atomic binary self-update. Mirrors gstack's mechanism. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(self-upgrade): write just-upgraded-from breadcrumb + clear stale cache after upgrade Codex ship-review P3: the CLI startup hook reads just-upgraded-from to print the one-time JUST_UPGRADED confirmation, but nothing wrote it — dead path. runUpgrade now writes the breadcrumb (covers full + --swap-only) and clears the update-check cache + snooze so a now-applied 'upgrade available' marker stops nudging. * feat(self-upgrade): surface what's-new in notify + wire agent integration (AGENTS.md, HEARTBEAT) + e2e - self-upgrade --check-only --json now includes changelog_diff + release_url (export fetchChangelog); the gbrain-upgrade skill shows 3-5 what's-new bullets before the 4-option prompt instead of just version numbers. - setup injects a self-upgrade marker protocol into AGENTS.md so interactive agents (Claude Code, Codex) act on the UPGRADE_AVAILABLE stderr marker — the piece that makes notify actually fire for them. - HEARTBEAT daily beat routes through the gbrain-upgrade skill (OpenClaw/Hermes cron cadence); auto-mode daemons ride the autopilot tick. - e2e: real subprocess invocation proves the marker fires (notify emits; off/snooze/up-to-date silent; JUST_UPGRADED fires+clears; --quiet suppresses). Serial test: --check-only surfaces the changelog. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
/**
|
|
* Serial (stubs globalThis.fetch): `gbrain self-upgrade --check-only --json`
|
|
* surfaces the changelog so the notify prompt can tell the operator WHAT they'll
|
|
* get, not just a version number. Network stubbed; the JSON shape + changelog
|
|
* extraction are real.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { VERSION } from '../src/version.ts';
|
|
import { parseSemver } from '../src/core/semver.ts';
|
|
import { runSelfUpgrade } from '../src/commands/self-upgrade.ts';
|
|
|
|
const realFetch = globalThis.fetch;
|
|
const realLog = console.log;
|
|
let home: string;
|
|
let priorHome: string | undefined;
|
|
let captured: string[];
|
|
|
|
function minorBump(): string {
|
|
const v = parseSemver(VERSION)!;
|
|
return `${v[0]}.${v[1] + 1}.0`;
|
|
}
|
|
|
|
function stub(tag: string | null, changelog: string): void {
|
|
globalThis.fetch = (async (url: any) => {
|
|
const u = String(url);
|
|
if (u.includes('/releases/latest')) {
|
|
if (tag === null) throw new Error('network down');
|
|
return new Response(JSON.stringify({ tag_name: tag, published_at: '2026-01-01', html_url: 'https://x/rel' }), { status: 200 });
|
|
}
|
|
if (u.includes('CHANGELOG.md')) return new Response(changelog, { status: 200 });
|
|
return new Response('', { status: 200 });
|
|
}) as typeof fetch;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
priorHome = process.env.GBRAIN_HOME;
|
|
home = mkdtempSync(join(tmpdir(), 'gbrain-checkonly-'));
|
|
process.env.GBRAIN_HOME = home;
|
|
captured = [];
|
|
console.log = (...a: unknown[]) => { captured.push(a.join(' ')); };
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = realFetch;
|
|
console.log = realLog;
|
|
if (priorHome === undefined) delete process.env.GBRAIN_HOME;
|
|
else process.env.GBRAIN_HOME = priorHome;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('self-upgrade --check-only surfaces what you get', () => {
|
|
test('behind → JSON includes changelog_diff + release_url + update_available', async () => {
|
|
const latest = minorBump();
|
|
const changelog = `# Changelog\n\n## [${latest}] - 2026-01-01\n\n- Shiny new thing\n- Another fix\n\n## [${VERSION}] - 2025-12-01\n\n- old\n`;
|
|
stub(`v${latest}`, changelog);
|
|
await runSelfUpgrade(['--check-only', '--json']);
|
|
const out = JSON.parse(captured.join('\n'));
|
|
expect(out.update_available).toBe(true);
|
|
expect(out.latest_version).toBe(latest);
|
|
expect(out.release_url).toBe('https://x/rel');
|
|
expect(out.changelog_diff).toContain('Shiny new thing');
|
|
});
|
|
|
|
test('behind, human output prints What changed', async () => {
|
|
const latest = minorBump();
|
|
const changelog = `# Changelog\n\n## [${latest}] - 2026-01-01\n\n- Headline feature\n\n## [${VERSION}] - 2025-12-01\n\n- old\n`;
|
|
stub(`v${latest}`, changelog);
|
|
await runSelfUpgrade(['--check-only']);
|
|
const text = captured.join('\n');
|
|
expect(text).toContain('What changed');
|
|
expect(text).toContain('Headline feature');
|
|
});
|
|
|
|
test('up to date → no changelog fetched, empty diff', async () => {
|
|
// Stub returns the SAME version → not behind → no changelog.
|
|
stub(`v${VERSION}`, 'should-not-be-read');
|
|
await runSelfUpgrade(['--check-only', '--json']);
|
|
const out = JSON.parse(captured.join('\n'));
|
|
expect(out.update_available).toBe(false);
|
|
expect(out.changelog_diff).toBe('');
|
|
});
|
|
|
|
test('network failure → up to date, no crash', async () => {
|
|
stub(null, '');
|
|
await runSelfUpgrade(['--check-only', '--json']);
|
|
const out = JSON.parse(captured.join('\n'));
|
|
expect(out.update_available).toBe(false);
|
|
expect(out.changelog_diff).toBe('');
|
|
});
|
|
});
|