mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 07:40:09 +02:00
feat(gbrain): add lib/gbrain-local-status classifier with 5-state engine status + 60s cache
Foundation for split-engine gbrain: shared classifier used by both
bin/gstack-gbrain-detect (preamble probe) and bin/gstack-gbrain-sync.ts
(orchestrator SKIP-when-not-ok). Single source of truth.
Probes via `gbrain sources list --json` and classifies stderr against the
same patterns lib/gbrain-sources.ts:66-67 already uses ("Cannot connect to
database", "config.json"). Returns one of: ok, no-cli, missing-config,
broken-config, broken-db. Defensive default: unrecognized failures
classify as broken-config so the raw stderr can be surfaced upstream.
Cache at ~/.gstack/.gbrain-local-status-cache.json keyed on
{home, path_hash, gbrain_bin_path, gbrain_version, config_mtime, config_size}
with 60s TTL. Cache invalidates on any invariant change. --no-cache option
busts the cache for callers that just mutated state (/setup-gbrain,
/sync-gbrain after init/migration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* gbrain-local-status — classify the local gbrain engine into 5 states.
|
||||
*
|
||||
* Shared between bin/gstack-gbrain-detect (preamble probe on every skill start)
|
||||
* and bin/gstack-gbrain-sync.ts (orchestrator SKIP-when-not-ok semantics).
|
||||
* Single source of truth: same probe, same classification, same cache.
|
||||
*
|
||||
* Per the split-engine plan (D2 + D8):
|
||||
* - Probe: `gbrain sources list --json`. Cheap (~80ms), actually hits the DB.
|
||||
* Uses the same stderr patterns as lib/gbrain-sources.ts:66-67.
|
||||
* - Cache: 60s TTL at ~/.gstack/.gbrain-local-status-cache.json, keyed on
|
||||
* {home, path_hash, gbrain_bin_path, gbrain_version, config_mtime}.
|
||||
* - --no-cache bypass: /setup-gbrain and /sync-gbrain pass it after any
|
||||
* state-mutating operation so the next read sees fresh status.
|
||||
*
|
||||
* No-cli → gbrain not on PATH.
|
||||
* Missing → CLI present, ~/.gbrain/config.json absent.
|
||||
* Broken-config → config exists but `gbrain sources list` fails with config parse error
|
||||
* (or any non-recognized error — defensive default per codex #8).
|
||||
* Broken-db → config exists, DB unreachable per stderr classification.
|
||||
* Ok → DB reachable, sources list returned valid JSON.
|
||||
*/
|
||||
|
||||
import { execFileSync } from "child_process";
|
||||
import {
|
||||
createHash,
|
||||
} from "crypto";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "fs";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
export type LocalEngineStatus =
|
||||
| "ok"
|
||||
| "no-cli"
|
||||
| "missing-config"
|
||||
| "broken-config"
|
||||
| "broken-db";
|
||||
|
||||
export interface ClassifyOptions {
|
||||
/** Bypass the 60s cache. Used after any state-mutating operation. */
|
||||
noCache?: boolean;
|
||||
/** Env override for the spawned `gbrain` (used by tests to point at a fake binary). */
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
schema_version: 1;
|
||||
status: LocalEngineStatus;
|
||||
cached_at: number;
|
||||
/** Cache invariants — entry is invalidated if any of these change between writes. */
|
||||
key: {
|
||||
home: string;
|
||||
path_hash: string;
|
||||
gbrain_bin_path: string;
|
||||
gbrain_version: string;
|
||||
config_mtime: number; // 0 when config absent
|
||||
config_size: number; // 0 when config absent
|
||||
};
|
||||
}
|
||||
|
||||
export const CACHE_TTL_MS = 60_000;
|
||||
export const PROBE_TIMEOUT_MS = 5_000;
|
||||
|
||||
/** Effective user home — respects HOME env override (used by tests). */
|
||||
function userHome(): string {
|
||||
return process.env.HOME || homedir();
|
||||
}
|
||||
|
||||
/** Cache path computed fresh on each call so tests can mutate GSTACK_HOME per case. */
|
||||
export function cacheFilePath(): string {
|
||||
return join(
|
||||
process.env.GSTACK_HOME || join(userHome(), ".gstack"),
|
||||
".gbrain-local-status-cache.json",
|
||||
);
|
||||
}
|
||||
|
||||
function gbrainConfigPath(): string {
|
||||
return join(userHome(), ".gbrain", "config.json");
|
||||
}
|
||||
|
||||
function hashPath(p: string): string {
|
||||
return createHash("sha256").update(p).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the absolute path of `gbrain` on PATH. Returns null when missing.
|
||||
* Uses `command -v` semantics via execFileSync.
|
||||
*/
|
||||
function resolveGbrainBin(env?: NodeJS.ProcessEnv): string | null {
|
||||
try {
|
||||
const out = execFileSync("sh", ["-c", "command -v gbrain"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 2_000,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
env: env ?? process.env,
|
||||
});
|
||||
return out.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readGbrainVersion(env?: NodeJS.ProcessEnv): string {
|
||||
try {
|
||||
const out = execFileSync("gbrain", ["--version"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 2_000,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
env: env ?? process.env,
|
||||
});
|
||||
return out.trim().split("\n")[0] || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function configFingerprint(): { mtime: number; size: number } {
|
||||
try {
|
||||
const st = statSync(gbrainConfigPath());
|
||||
return { mtime: Math.floor(st.mtimeMs), size: st.size };
|
||||
} catch {
|
||||
return { mtime: 0, size: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function buildCacheKey(
|
||||
gbrainBin: string | null,
|
||||
gbrainVersion: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
): CacheEntry["key"] {
|
||||
const e = env ?? process.env;
|
||||
const config = configFingerprint();
|
||||
return {
|
||||
home: e.HOME || "",
|
||||
path_hash: hashPath(e.PATH || ""),
|
||||
gbrain_bin_path: gbrainBin || "",
|
||||
gbrain_version: gbrainVersion,
|
||||
config_mtime: config.mtime,
|
||||
config_size: config.size,
|
||||
};
|
||||
}
|
||||
|
||||
function keysEqual(a: CacheEntry["key"], b: CacheEntry["key"]): boolean {
|
||||
return (
|
||||
a.home === b.home &&
|
||||
a.path_hash === b.path_hash &&
|
||||
a.gbrain_bin_path === b.gbrain_bin_path &&
|
||||
a.gbrain_version === b.gbrain_version &&
|
||||
a.config_mtime === b.config_mtime &&
|
||||
a.config_size === b.config_size
|
||||
);
|
||||
}
|
||||
|
||||
function readCache(key: CacheEntry["key"]): LocalEngineStatus | null {
|
||||
if (!existsSync(cacheFilePath())) return null;
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(cacheFilePath(), "utf-8")) as CacheEntry;
|
||||
if (raw.schema_version !== 1) return null;
|
||||
if (Date.now() - raw.cached_at > CACHE_TTL_MS) return null;
|
||||
if (!keysEqual(raw.key, key)) return null;
|
||||
return raw.status;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(status: LocalEngineStatus, key: CacheEntry["key"]): void {
|
||||
const entry: CacheEntry = {
|
||||
schema_version: 1,
|
||||
status,
|
||||
cached_at: Date.now(),
|
||||
key,
|
||||
};
|
||||
try {
|
||||
mkdirSync(dirname(cacheFilePath()), { recursive: true });
|
||||
const tmp = cacheFilePath() + ".tmp." + process.pid;
|
||||
writeFileSync(tmp, JSON.stringify(entry, null, 2), "utf-8");
|
||||
renameSync(tmp, cacheFilePath());
|
||||
} catch {
|
||||
// Cache write failure is non-fatal — we re-probe next call.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe via `gbrain sources list --json`. Classify the outcome.
|
||||
*
|
||||
* Pattern strings ("Cannot connect to database", "config.json") are deliberately
|
||||
* the same strings used in lib/gbrain-sources.ts:66-67. If gbrain reworks its
|
||||
* error messages, classifier returns broken-config defensively (codex #8).
|
||||
*/
|
||||
function freshClassify(env?: NodeJS.ProcessEnv): LocalEngineStatus {
|
||||
// 1. CLI on PATH?
|
||||
const gbrainBin = resolveGbrainBin(env);
|
||||
if (!gbrainBin) return "no-cli";
|
||||
|
||||
// 2. Config file present?
|
||||
if (!existsSync(gbrainConfigPath())) return "missing-config";
|
||||
|
||||
// 3. Probe gbrain sources list.
|
||||
try {
|
||||
execFileSync("gbrain", ["sources", "list", "--json"], {
|
||||
encoding: "utf-8",
|
||||
timeout: PROBE_TIMEOUT_MS,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: env ?? process.env,
|
||||
});
|
||||
return "ok";
|
||||
} catch (err) {
|
||||
const e = err as NodeJS.ErrnoException & { stderr?: Buffer | string };
|
||||
const stderr = (e.stderr ? e.stderr.toString() : "") || "";
|
||||
|
||||
// ENOENT can happen if gbrain disappeared between resolveGbrainBin and now.
|
||||
if (e.code === "ENOENT") return "no-cli";
|
||||
|
||||
// Pattern match against gbrain's known error strings. Order matters:
|
||||
// "Cannot connect to database" is the more specific DB-unreachable signal.
|
||||
if (stderr.includes("Cannot connect to database")) return "broken-db";
|
||||
if (stderr.includes("config.json")) return "broken-config";
|
||||
|
||||
// Defensive default per codex #8: unrecognized failures classify as
|
||||
// broken-config so the user sees the raw stderr surfaced upstream.
|
||||
return "broken-config";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify the local gbrain engine status. Cached for 60s; bypassable.
|
||||
*
|
||||
* Returns one of 5 states. Never throws — failure modes are surfaced as states.
|
||||
*/
|
||||
export function localEngineStatus(opts: ClassifyOptions = {}): LocalEngineStatus {
|
||||
const env = opts.env ?? process.env;
|
||||
const gbrainBin = resolveGbrainBin(env);
|
||||
const gbrainVersion = gbrainBin ? readGbrainVersion(env) : "";
|
||||
const key = buildCacheKey(gbrainBin, gbrainVersion, env);
|
||||
|
||||
if (!opts.noCache) {
|
||||
const cached = readCache(key);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
const fresh = freshClassify(env);
|
||||
writeCache(fresh, key);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Unit tests for lib/gbrain-local-status.ts.
|
||||
*
|
||||
* Per the eng-review D6 (gate-tier = mocked, codex #9): no real gbrain CLI, no
|
||||
* real PGLite, no real Postgres. Each case builds a fake `gbrain` shell script
|
||||
* on PATH that emits canned exit codes + stderr matching the patterns the
|
||||
* classifier looks for.
|
||||
*
|
||||
* Five status cases:
|
||||
* 1. no-cli — gbrain absent from PATH
|
||||
* 2. missing-config — gbrain present, ~/.gbrain/config.json absent
|
||||
* 3. broken-config — gbrain present, config exists, stderr contains "config.json"
|
||||
* 4. broken-db — gbrain present, config exists, stderr contains "Cannot connect to database"
|
||||
* 5. ok — gbrain present, config exists, sources list returns valid JSON
|
||||
*
|
||||
* Plus cache behavior: hit, TTL expiry, invariant invalidation (HOME change),
|
||||
* --no-cache bypass.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import {
|
||||
mkdtempSync,
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
rmSync,
|
||||
chmodSync,
|
||||
existsSync,
|
||||
utimesSync,
|
||||
} from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
import {
|
||||
localEngineStatus,
|
||||
cacheFilePath,
|
||||
CACHE_TTL_MS,
|
||||
type LocalEngineStatus,
|
||||
} from "../lib/gbrain-local-status";
|
||||
|
||||
interface FakeEnv {
|
||||
tmp: string;
|
||||
bindir: string;
|
||||
home: string;
|
||||
gstackHome: string;
|
||||
configPath: string;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a tmp HOME + GSTACK_HOME + optional fake `gbrain` on PATH.
|
||||
*
|
||||
* The classifier reads HOME via os.homedir() which reads process.env.HOME, so
|
||||
* we mutate process.env ambiently in each test (restored in afterEach).
|
||||
*/
|
||||
function makeEnv(opts: {
|
||||
withGbrain?: boolean;
|
||||
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "throws";
|
||||
withConfig?: boolean;
|
||||
}): FakeEnv {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gbrain-local-status-test-"));
|
||||
const bindir = join(tmp, "bin");
|
||||
const home = join(tmp, "home");
|
||||
const gstackHome = join(home, ".gstack");
|
||||
const configDir = join(home, ".gbrain");
|
||||
const configPath = join(configDir, "config.json");
|
||||
|
||||
mkdirSync(bindir, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(gstackHome, { recursive: true });
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
if (opts.withConfig) {
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ engine: "pglite", database_url: "pglite:///fake" }),
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.withGbrain) {
|
||||
const behavior = opts.gbrainBehavior || "ok";
|
||||
const fake = makeFakeGbrainScript(behavior);
|
||||
const gbrainPath = join(bindir, "gbrain");
|
||||
writeFileSync(gbrainPath, fake);
|
||||
chmodSync(gbrainPath, 0o755);
|
||||
}
|
||||
|
||||
return {
|
||||
tmp,
|
||||
bindir,
|
||||
home,
|
||||
gstackHome,
|
||||
configPath,
|
||||
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
function makeFakeGbrainScript(
|
||||
behavior: "ok" | "broken-db" | "broken-config" | "throws",
|
||||
): string {
|
||||
const stderrLine =
|
||||
behavior === "broken-db"
|
||||
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
|
||||
: behavior === "broken-config"
|
||||
? 'echo "Error: malformed config.json at ~/.gbrain/config.json" >&2'
|
||||
: behavior === "throws"
|
||||
? 'echo "unexpected gbrain failure" >&2'
|
||||
: "";
|
||||
const exitCode = behavior === "ok" ? 0 : 1;
|
||||
return `#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then
|
||||
echo "gbrain 0.33.1.0"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1 $2" = "sources list" ]; then
|
||||
if [ ${exitCode} -eq 0 ]; then
|
||||
echo '{"sources":[]}'
|
||||
exit 0
|
||||
fi
|
||||
${stderrLine}
|
||||
exit ${exitCode}
|
||||
fi
|
||||
exit 0
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a FakeEnv to process.env. Returns a function that restores previous values.
|
||||
*
|
||||
* PATH is REPLACED (not prepended) so a real `gbrain` on the inherited PATH
|
||||
* can't shadow the test's fake-or-absent binary. /usr/bin:/bin is kept so `sh`
|
||||
* and `command` work.
|
||||
*/
|
||||
function applyEnv(env: FakeEnv): () => void {
|
||||
const prev = {
|
||||
HOME: process.env.HOME,
|
||||
PATH: process.env.PATH,
|
||||
GSTACK_HOME: process.env.GSTACK_HOME,
|
||||
};
|
||||
process.env.HOME = env.home;
|
||||
process.env.PATH = `${env.bindir}:/usr/bin:/bin`;
|
||||
process.env.GSTACK_HOME = env.gstackHome;
|
||||
return () => {
|
||||
if (prev.HOME === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = prev.HOME;
|
||||
if (prev.PATH === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = prev.PATH;
|
||||
if (prev.GSTACK_HOME === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = prev.GSTACK_HOME;
|
||||
};
|
||||
}
|
||||
|
||||
describe("lib/gbrain-local-status — five status cases", () => {
|
||||
let env: FakeEnv | null = null;
|
||||
let restoreEnv: (() => void) | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (restoreEnv) restoreEnv();
|
||||
if (env) env.cleanup();
|
||||
env = null;
|
||||
restoreEnv = null;
|
||||
});
|
||||
|
||||
it("returns 'no-cli' when gbrain is not on PATH", () => {
|
||||
env = makeEnv({ withGbrain: false });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: true })).toBe("no-cli");
|
||||
});
|
||||
|
||||
it("returns 'missing-config' when CLI is present but ~/.gbrain/config.json absent", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: false });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: true })).toBe("missing-config");
|
||||
});
|
||||
|
||||
it("returns 'broken-db' when sources list emits 'Cannot connect to database'", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-db", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: true })).toBe("broken-db");
|
||||
});
|
||||
|
||||
it("returns 'broken-config' when sources list emits config.json error", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-config", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: true })).toBe("broken-config");
|
||||
});
|
||||
|
||||
it("returns 'broken-config' defensively when stderr matches neither pattern", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "throws", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: true })).toBe("broken-config");
|
||||
});
|
||||
|
||||
it("returns 'ok' when sources list succeeds", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: true })).toBe("ok");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lib/gbrain-local-status — cache behavior", () => {
|
||||
let env: FakeEnv | null = null;
|
||||
let restoreEnv: (() => void) | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (restoreEnv) restoreEnv();
|
||||
if (env) env.cleanup();
|
||||
env = null;
|
||||
restoreEnv = null;
|
||||
});
|
||||
|
||||
it("writes a cache entry on first call", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
localEngineStatus({ noCache: false });
|
||||
expect(existsSync(cacheFilePath())).toBe(true);
|
||||
});
|
||||
|
||||
it("returns cached value within TTL even if underlying state would change", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
const first = localEngineStatus({ noCache: false });
|
||||
expect(first).toBe("ok");
|
||||
|
||||
// Make the fake gbrain emit broken-db now. Cache should still say ok.
|
||||
writeFileSync(
|
||||
join(env.bindir, "gbrain"),
|
||||
makeFakeGbrainScript("broken-db"),
|
||||
);
|
||||
chmodSync(join(env.bindir, "gbrain"), 0o755);
|
||||
|
||||
const second = localEngineStatus({ noCache: false });
|
||||
expect(second).toBe("ok"); // cache hit
|
||||
});
|
||||
|
||||
it("re-probes when --no-cache is passed", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: false })).toBe("ok");
|
||||
|
||||
writeFileSync(
|
||||
join(env.bindir, "gbrain"),
|
||||
makeFakeGbrainScript("broken-db"),
|
||||
);
|
||||
chmodSync(join(env.bindir, "gbrain"), 0o755);
|
||||
|
||||
expect(localEngineStatus({ noCache: true })).toBe("broken-db");
|
||||
});
|
||||
|
||||
it("invalidates cache when config_mtime changes (key invariant)", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: false })).toBe("ok");
|
||||
|
||||
// Bump config mtime artificially (touch +10s) AND rewrite gbrain to broken-db.
|
||||
const future = Math.floor(Date.now() / 1000) + 10;
|
||||
utimesSync(env.configPath, future, future);
|
||||
writeFileSync(
|
||||
join(env.bindir, "gbrain"),
|
||||
makeFakeGbrainScript("broken-db"),
|
||||
);
|
||||
chmodSync(join(env.bindir, "gbrain"), 0o755);
|
||||
|
||||
// Even with cache enabled, mtime mismatch forces re-probe.
|
||||
expect(localEngineStatus({ noCache: false })).toBe("broken-db");
|
||||
});
|
||||
|
||||
it("invalidates cache when HOME changes (key invariant)", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: false })).toBe("ok");
|
||||
|
||||
// Switch to a new HOME (different user). Same gstack home (shared cache file).
|
||||
const env2 = makeEnv({
|
||||
withGbrain: true,
|
||||
gbrainBehavior: "broken-db",
|
||||
withConfig: true,
|
||||
});
|
||||
process.env.HOME = env2.home;
|
||||
process.env.PATH = `${env2.bindir}:/usr/bin:/bin`;
|
||||
// GSTACK_HOME stays pointing at env.gstackHome (the original cache file).
|
||||
|
||||
try {
|
||||
expect(localEngineStatus({ noCache: false })).toBe("broken-db");
|
||||
} finally {
|
||||
env2.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user