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>
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
/**
|
|
* v0.31.1 (Issue #734): tests for `get_brain_identity` MCP op.
|
|
*
|
|
* The op is the data source for the thin-client identity banner. Returns
|
|
* {version, engine, page_count, chunk_count, last_sync_iso}. Read-scope.
|
|
* Reuses engine.getStats() — banner's 60s TTL cache bounds frequency.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import { operationsByName } from '../src/core/operations.ts';
|
|
import { VERSION } from '../src/version.ts';
|
|
import type { OperationContext } from '../src/core/operations.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
});
|
|
|
|
function buildCtx(): OperationContext {
|
|
return {
|
|
engine,
|
|
config: {} as any,
|
|
logger: console,
|
|
dryRun: false,
|
|
remote: false,
|
|
sourceId: 'default',
|
|
};
|
|
}
|
|
|
|
describe('get_brain_identity op', () => {
|
|
test('is registered in operationsByName', () => {
|
|
expect(operationsByName.get_brain_identity).toBeDefined();
|
|
});
|
|
|
|
test('declares scope=read so non-admin clients can call it', () => {
|
|
expect(operationsByName.get_brain_identity.scope).toBe('read');
|
|
});
|
|
|
|
test('is NOT localOnly (must be reachable over MCP for thin-client banner)', () => {
|
|
expect(operationsByName.get_brain_identity.localOnly).toBeFalsy();
|
|
});
|
|
|
|
test('takes no params', () => {
|
|
expect(operationsByName.get_brain_identity.params).toEqual({});
|
|
});
|
|
|
|
test('has no cliHints — banner-only op, not user-facing CLI surface', () => {
|
|
expect(operationsByName.get_brain_identity.cliHints).toBeUndefined();
|
|
});
|
|
|
|
test('returns identity packet with VERSION + engine kind on empty brain', async () => {
|
|
const op = operationsByName.get_brain_identity;
|
|
const result = (await op.handler(buildCtx(), {})) as {
|
|
version: string;
|
|
engine: 'postgres' | 'pglite';
|
|
page_count: number;
|
|
chunk_count: number;
|
|
last_sync_iso: string | null;
|
|
};
|
|
|
|
expect(result.version).toBe(VERSION);
|
|
expect(result.engine).toBe('pglite');
|
|
expect(result.page_count).toBe(0);
|
|
expect(result.chunk_count).toBe(0);
|
|
expect(result.last_sync_iso).toBe(null);
|
|
});
|
|
|
|
test('reflects page count after pages are seeded', async () => {
|
|
await engine.putPage('wiki/test/foo', {
|
|
type: 'note',
|
|
title: 'Foo',
|
|
compiled_truth: 'foo body',
|
|
});
|
|
await engine.putPage('wiki/test/bar', {
|
|
type: 'note',
|
|
title: 'Bar',
|
|
compiled_truth: 'bar body',
|
|
});
|
|
|
|
const op = operationsByName.get_brain_identity;
|
|
const result = (await op.handler(buildCtx(), {})) as {
|
|
page_count: number;
|
|
chunk_count: number;
|
|
};
|
|
|
|
expect(result.page_count).toBe(2);
|
|
// chunk_count depends on chunker; just assert it advanced past zero
|
|
expect(result.chunk_count).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test('returns stable shape (load-bearing for thin-client banner)', async () => {
|
|
const op = operationsByName.get_brain_identity;
|
|
const result = (await op.handler(buildCtx(), {})) as Record<string, unknown>;
|
|
const keys = Object.keys(result).sort();
|
|
expect(keys).toEqual([
|
|
'chunk_count',
|
|
'engine',
|
|
'last_sync_iso',
|
|
'latest_version',
|
|
'page_count',
|
|
'update_available',
|
|
'version',
|
|
]);
|
|
});
|
|
|
|
test('last_sync_iso is null in v0.31.1 (deferred to v0.31.x — see plan)', async () => {
|
|
// Documents the known gap so an implementer who later wires the cycle
|
|
// to write a sync timestamp can flip this expectation.
|
|
const op = operationsByName.get_brain_identity;
|
|
const result = (await op.handler(buildCtx(), {})) as { last_sync_iso: string | null };
|
|
expect(result.last_sync_iso).toBe(null);
|
|
});
|
|
});
|