v0.42.66.0 fix(onboard): honor file-plane schema pack in checks (#2538) (#3396)

* fix(onboard): resolve pack checks with file config

* test(onboard): sandbox GBRAIN_HOME in pre-existing pack-check tests

The fix routes checkPackUpgradeAvailable/checkTypeProliferation through
loadConfigFileOnly(), so the file's pre-existing tests now read the real
~/.gbrain/config.json and fail on any machine whose config sets
schema_pack. Wrap them in withEnv({ GBRAIN_HOME: emptyHome(), ... }),
matching the new test's idiom.

Co-authored-by: javieraldape <javieraldape@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: gbrain-contrib <gbrain-contrib@example.com>
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Javier Aldape
2026-07-27 14:15:35 -07:00
committed by GitHub
co-authored by Claude Fable 5 gbrain-contrib Garry Tan
parent b6c75d802f
commit 3126b8fdfc
2 changed files with 65 additions and 29 deletions
+6 -4
View File
@@ -386,14 +386,15 @@ export async function checkPackUpgradeAvailable(
): Promise<OnboardCheckResult> {
try {
const { loadActivePack, findPackSuccessors } = await import('../schema-pack/load-active.ts');
const { loadConfigFileOnly } = await import('../config.ts');
// Read the engine's DB-side schema_pack so a post-unify flip is visible
// here even before the file-plane config catches up. Falls through to
// file-plane/env/default resolution when unset.
// here even before the file-plane config catches up. File-only config
// preserves tier-6 schema_pack without merging transient env/database state.
let dbConfig: string | undefined;
try {
dbConfig = (await engine.getConfig('schema_pack')) ?? undefined;
} catch { /* engine.config may not exist on very old brains */ }
const active = await loadActivePack({ cfg: null, remote: false, dbConfig })
const active = await loadActivePack({ cfg: loadConfigFileOnly(), remote: false, dbConfig })
.catch(() => null);
if (!active) {
return {
@@ -463,11 +464,12 @@ export async function checkTypeProliferation(
let declared = 15; // fallback to gbrain-base-v2 default if pack unavailable
try {
const { loadActivePack } = await import('../schema-pack/load-active.ts');
const { loadConfigFileOnly } = await import('../config.ts');
let dbConfig: string | undefined;
try {
dbConfig = (await engine.getConfig('schema_pack')) ?? undefined;
} catch { /* tolerate pre-config brains */ }
const active = await loadActivePack({ cfg: null, remote: false, dbConfig })
const active = await loadActivePack({ cfg: loadConfigFileOnly(), remote: false, dbConfig })
.catch(() => null);
if (active) declared = active.manifest.page_types.length;
} catch {
+59 -25
View File
@@ -5,8 +5,12 @@
// JOIN (F12); manual_only RemediationStep flag round-trips through render.
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { resetPgliteState } from './helpers/reset-pglite.ts';
import { emptyHome, withEnv } from './helpers/with-env.ts';
import {
checkPackUpgradeAvailable,
checkTypeProliferation,
@@ -56,29 +60,57 @@ describe('checkPackUpgradeAvailable', () => {
it('fires on gbrain-base brain with gbrain-base-v2 available', async () => {
// Default active pack is gbrain-base; gbrain-base-v2 declares
// migration_from: {pack: gbrain-base, version: "1.x"}.
const result = await checkPackUpgradeAvailable(engine);
expect(result.check.name).toBe('pack_upgrade_available');
expect(result.check.status).toBe('warn');
expect(result.check.message).toContain('gbrain-base-v2');
expect(result.remediations.length).toBe(1);
expect(result.remediations[0].job).toBe('unify-types');
expect(result.remediations[0].protected).toBe(true);
expect(result.remediations[0].params.target_pack).toBe('gbrain-base-v2');
// Sandbox GBRAIN_HOME: the check reads file-plane config, so a dev
// machine whose real ~/.gbrain/config.json sets schema_pack would
// flip this assertion.
await withEnv({ GBRAIN_HOME: emptyHome(), GBRAIN_SCHEMA_PACK: undefined }, async () => {
const result = await checkPackUpgradeAvailable(engine);
expect(result.check.name).toBe('pack_upgrade_available');
expect(result.check.status).toBe('warn');
expect(result.check.message).toContain('gbrain-base-v2');
expect(result.remediations.length).toBe(1);
expect(result.remediations[0].job).toBe('unify-types');
expect(result.remediations[0].protected).toBe(true);
expect(result.remediations[0].params.target_pack).toBe('gbrain-base-v2');
});
});
it('honors file-plane schema_pack when DB config is unset', async () => {
const home = mkdtempSync(join(tmpdir(), 'gbrain-pack-upgrade-'));
const configDir = join(home, '.gbrain');
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, 'config.json'),
JSON.stringify({ schema_pack: 'gbrain-base-v2' }, null, 2),
);
await withEnv({ GBRAIN_HOME: home, GBRAIN_SCHEMA_PACK: undefined }, async () => {
_resetPackCacheForTests();
const result = await checkPackUpgradeAvailable(engine);
expect(result.check.name).toBe('pack_upgrade_available');
expect(result.check.status).toBe('ok');
expect(result.check.message).toContain('gbrain-base-v2');
expect(result.remediations).toEqual([]);
});
});
it('manual_only routing via render.ts allowlist (D17)', async () => {
const result = await checkPackUpgradeAvailable(engine);
const step = result.remediations[0];
const rec = toOnboardRecommendation(step);
expect(rec.apply_policy).toBe('manual_only');
await withEnv({ GBRAIN_HOME: emptyHome(), GBRAIN_SCHEMA_PACK: undefined }, async () => {
const result = await checkPackUpgradeAvailable(engine);
const step = result.remediations[0];
const rec = toOnboardRecommendation(step);
expect(rec.apply_policy).toBe('manual_only');
});
});
});
describe('checkTypeProliferation (D16 pack-aware ratio)', () => {
it('returns ok when distinct types under declared+5 threshold', async () => {
await seedPages(['note', 'meeting', 'slack']);
const result = await checkTypeProliferation(engine);
expect(result.check.status).toBe('ok');
await withEnv({ GBRAIN_HOME: emptyHome(), GBRAIN_SCHEMA_PACK: undefined }, async () => {
const result = await checkTypeProliferation(engine);
expect(result.check.status).toBe('ok');
});
});
it('warns when distinct types exceed declared+5', async () => {
@@ -86,17 +118,19 @@ describe('checkTypeProliferation (D16 pack-aware ratio)', () => {
// the same way checkTypeProliferation does, then seed declared+6 so the
// test keeps passing when the base pack grows (e.g. #2390 added
// event + diary and silently moved the fixed threshold).
const { loadActivePack } = await import('../src/core/schema-pack/load-active.ts');
const dbConfig = (await engine.getConfig('schema_pack')) ?? undefined;
const active = await loadActivePack({ cfg: null, remote: false, dbConfig }).catch(() => null);
const declared = active ? active.manifest.page_types.length : 15;
const seedCount = declared + 6; // one past the warn threshold (declared+5)
const types: string[] = [];
for (let i = 0; i < seedCount; i++) types.push(`custom-type-${i}`);
await seedPages(types);
const result = await checkTypeProliferation(engine);
expect(result.check.status).toBe('warn');
expect(result.check.message).toMatch(new RegExp(`${seedCount} distinct`));
await withEnv({ GBRAIN_HOME: emptyHome(), GBRAIN_SCHEMA_PACK: undefined }, async () => {
const { loadActivePack } = await import('../src/core/schema-pack/load-active.ts');
const dbConfig = (await engine.getConfig('schema_pack')) ?? undefined;
const active = await loadActivePack({ cfg: null, remote: false, dbConfig }).catch(() => null);
const declared = active ? active.manifest.page_types.length : 15;
const seedCount = declared + 6; // one past the warn threshold (declared+5)
const types: string[] = [];
for (let i = 0; i < seedCount; i++) types.push(`custom-type-${i}`);
await seedPages(types);
const result = await checkTypeProliferation(engine);
expect(result.check.status).toBe('warn');
expect(result.check.message).toMatch(new RegExp(`${seedCount} distinct`));
});
});
});