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>
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
/**
|
|
* E2E: the self-upgrade marker actually fires on a real `gbrain` invocation.
|
|
*
|
|
* This is the load-bearing proof of the "rides invocation frequency" mechanism:
|
|
* spawn a real `bun src/cli.ts <cmd>` with a warm "upgrade available" cache and
|
|
* assert the startup hook prints `UPGRADE_AVAILABLE` on stderr — which is what an
|
|
* agent (Claude Code / Codex / OpenClaw) keys off to run the gbrain-upgrade skill.
|
|
*
|
|
* Carrier command: `config get self_upgrade.mode` — runs the startup hook, needs
|
|
* no DB and no network, exits fast. The cache is pre-written fresh so the hook
|
|
* emits from cache and never spawns the detached network refresh (hermetic).
|
|
*
|
|
* The child is spawned with NODE_ENV unset (the production code gates the hook
|
|
* off under NODE_ENV=test to keep the unit suite from spawning refreshers).
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { VERSION } from '../../src/version.ts';
|
|
|
|
let home: string;
|
|
let gbrainDir: string;
|
|
const repoRoot = process.cwd();
|
|
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), 'gbrain-marker-'));
|
|
gbrainDir = join(home, '.gbrain');
|
|
mkdirSync(gbrainDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
function writeCache(line: string): void {
|
|
writeFileSync(join(gbrainDir, 'last-update-check'), line + '\n'); // mtime = now → fresh
|
|
}
|
|
|
|
function runGbrain(mode: string | undefined): { stdout: string; stderr: string; code: number } {
|
|
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
delete env.NODE_ENV; // un-gate the startup hook (production code skips it under test)
|
|
delete env.GBRAIN_SKIP_STARTUP_HOOKS;
|
|
env.GBRAIN_HOME = home;
|
|
if (mode === undefined) delete env.GBRAIN_SELF_UPGRADE_MODE;
|
|
else env.GBRAIN_SELF_UPGRADE_MODE = mode;
|
|
const r = Bun.spawnSync(['bun', 'src/cli.ts', 'config', 'get', 'self_upgrade.mode'], {
|
|
cwd: repoRoot,
|
|
env,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
return {
|
|
stdout: r.stdout.toString(),
|
|
stderr: r.stderr.toString(),
|
|
code: r.exitCode ?? -1,
|
|
};
|
|
}
|
|
|
|
describe('self-upgrade marker on a real invocation', () => {
|
|
test('notify mode + fresh upgrade_available cache → emits UPGRADE_AVAILABLE on stderr', () => {
|
|
writeCache(`UPGRADE_AVAILABLE ${VERSION} 0.99.0`);
|
|
const { stderr } = runGbrain('notify');
|
|
expect(stderr).toContain(`UPGRADE_AVAILABLE ${VERSION} 0.99.0`);
|
|
expect(stderr).toContain('Run: gbrain self-upgrade');
|
|
});
|
|
|
|
test('off mode → no marker (update checks disabled)', () => {
|
|
writeCache(`UPGRADE_AVAILABLE ${VERSION} 0.99.0`);
|
|
const { stderr } = runGbrain('off');
|
|
expect(stderr).not.toContain('UPGRADE_AVAILABLE');
|
|
});
|
|
|
|
test('up_to_date cache → no marker', () => {
|
|
writeCache(`UP_TO_DATE ${VERSION}`);
|
|
const { stderr } = runGbrain('notify');
|
|
expect(stderr).not.toContain('UPGRADE_AVAILABLE');
|
|
});
|
|
|
|
test('active snooze for the version → no marker (notify mode honors snooze)', () => {
|
|
writeCache(`UPGRADE_AVAILABLE ${VERSION} 0.99.0`);
|
|
// snooze record: "<version> <level> <epoch-ms>" — fresh ts so it's active.
|
|
writeFileSync(join(gbrainDir, 'update-snoozed'), `0.99.0 1 ${Date.now()}\n`);
|
|
const { stderr } = runGbrain('notify');
|
|
expect(stderr).not.toContain('UPGRADE_AVAILABLE');
|
|
});
|
|
|
|
test('JUST_UPGRADED breadcrumb → one-time confirmation on stderr, then cleared', () => {
|
|
const breadcrumb = join(gbrainDir, 'just-upgraded-from');
|
|
writeFileSync(breadcrumb, '0.42.0\n');
|
|
const { stderr } = runGbrain('notify');
|
|
expect(stderr).toContain(`JUST_UPGRADED 0.42.0 ${VERSION}`);
|
|
expect(existsSync(breadcrumb)).toBe(false); // consumed
|
|
});
|
|
|
|
test('--quiet suppresses the marker', () => {
|
|
writeCache(`UPGRADE_AVAILABLE ${VERSION} 0.99.0`);
|
|
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
delete env.NODE_ENV;
|
|
env.GBRAIN_HOME = home;
|
|
env.GBRAIN_SELF_UPGRADE_MODE = 'notify';
|
|
const r = Bun.spawnSync(['bun', 'src/cli.ts', '--quiet', 'config', 'get', 'self_upgrade.mode'], {
|
|
cwd: repoRoot,
|
|
env,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
expect(r.stderr.toString()).not.toContain('UPGRADE_AVAILABLE');
|
|
});
|
|
});
|