Skip to content

Commit 11a5b70

Browse files
committed
fix: CWD-independent test DB-isolation backstop (prevents live-DB migration)
ROOT CAUSE of the live-DB v41 fail-close incident (a re-run of the 2026-06-01 v26 incident): the MAGIC_CONTEXT_TEST_DATA_DIR isolation that keeps `bun test` off the user's REAL shared DB (~/.local/share/cortexkit/magic-context/context.db) is set by test-preload.ts, wired ONLY through each package's bunfig.toml `[test] preload`. Two CWD holes: - the monorepo ROOT bunfig.toml had no `[test] preload`, so a bare `bun test` from the repo/worktree root recursively ran every package's *.test.ts with NO isolation; - packages/cli had no bunfig.toml at all. A D19 mason worktree at LATEST_SUPPORTED_VERSION=41 ran `bun test`; an unisolated run called bare openDatabase() → resolved to the real DB → ran migration v41 on it → the user's v40 binaries fail-closed on restart. (v41 only ensureColumn'd one nullable TEXT column, so the data was safe; user rolled the migrations row back.) CLASS-ELIMINATION FIX (not a per-CWD patch): Bun sets NODE_ENV=test for EVERY `bun test` regardless of CWD/bunfig (verified), and it is never "test" in the plugin runtime. resolveDatabasePath() now has a backstop: when NODE_ENV=test and neither MAGIC_CONTEXT_TEST_DATA_DIR nor XDG_DATA_HOME is set (the dangerous window), it redirects the DB into a memoized throwaway temp dir so the real shared DB is physically unreachable from ANY CWD — even a brand-new package that forgets its bunfig. Empirically proven: with the preload env deleted mid-test, resolveDatabasePath returns a mc-test-db-backstop-* path, never ~/.local/share. Defense in depth: added `[test] preload` to the root bunfig.toml and a new packages/cli/bunfig.toml (both pointing at the canonical plugin test-preload.ts). Added a structural guard test asserting (a) the backstop redirects when unisolated, (b) a test's own XDG_DATA_HOME is still honored, and (c) all four test packages wire the preload — so a future package that forgets it fails CI. Gate: plugin 2227/0 (+14), CLI 185/0, Pi 487/0, tsc + biome clean.
1 parent 65fe58f commit 11a5b70

4 files changed

Lines changed: 140 additions & 5 deletions

File tree

bunfig.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@
99

1010
[install]
1111
minimumReleaseAge = 259200
12+
13+
# Test isolation for a bare `bun test` run from the monorepo ROOT (which
14+
# recursively discovers every package's *.test.ts). Without this, a root-CWD
15+
# `bun test` runs all suites with NO preload and a bare openDatabase() would hit
16+
# the user's REAL shared DB — exactly how a worktree at a newer LATEST migrated
17+
# the live DB (2026-06-01 v26 incident, recurred at v41). resolveDatabasePath()
18+
# also has a NODE_ENV=test backstop, but this keeps the redirect canonical.
19+
[test]
20+
preload = ["./packages/plugin/test-preload.ts"]

packages/cli/bunfig.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Supply-chain protection (mirrors the root bunfig): refuse Bun-installed
2+
# packages published within the last 3 days.
3+
[install]
4+
minimumReleaseAge = 259200
5+
6+
# Test isolation: force the shared cortexkit DB onto a throwaway temp dir for the
7+
# whole test process so NO cli test can read or migrate the user's real shared
8+
# database (shared with OpenCode + Pi). The CLI imports plugin storage via
9+
# @magic-context/core, so a bare openDatabase() here would otherwise hit the live
10+
# DB. resolveDatabasePath() also has a NODE_ENV=test backstop; this keeps the
11+
# redirect canonical. See packages/plugin/test-preload.ts for the full rationale.
12+
[test]
13+
preload = ["../plugin/test-preload.ts"]

packages/plugin/src/features/magic-context/storage-db.test.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/// <reference types="bun-types" />
22

33
import { afterEach, describe, expect, it } from "bun:test";
4-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5-
import { tmpdir } from "node:os";
4+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5+
import { homedir, tmpdir } from "node:os";
66
import { join } from "node:path";
77
import { Database } from "../../shared/sqlite";
88
import { closeQuietly } from "../../shared/sqlite-helpers";
9-
import { closeDatabase, isDatabasePersisted, openDatabase } from "./storage-db";
9+
import {
10+
closeDatabase,
11+
isDatabasePersisted,
12+
openDatabase,
13+
resolveDatabasePath,
14+
} from "./storage-db";
1015
import { clearSession } from "./storage-meta-session";
1116

1217
const tempDirs: string[] = [];
@@ -302,4 +307,59 @@ describe("storage-db", () => {
302307
expect(() => closeDatabase()).not.toThrow();
303308
});
304309
});
310+
311+
// Regression guard for the 2026-06-01 (v26) / 2026-06-19 (v41) incidents:
312+
// a `bun test` run from a CWD whose bunfig lacks `[test] preload` ran the
313+
// package suites with NO isolation, so a bare openDatabase() migrated the
314+
// user's REAL shared DB. The NODE_ENV=test backstop in resolveDatabasePath
315+
// makes that structurally impossible from ANY CWD.
316+
describe("#given the test-isolation backstop", () => {
317+
const realStorageRoot = join(homedir(), ".local", "share", "cortexkit");
318+
319+
it("#when NODE_ENV=test and XDG_DATA_HOME unset #then never resolves to the real shared DB", () => {
320+
// Simulate an UNISOLATED run: no preload-set vars at all.
321+
const savedXdg = process.env.XDG_DATA_HOME;
322+
const savedTestDir = process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
323+
process.env.NODE_ENV = "test";
324+
// biome-ignore lint/performance/noDelete: must be UNSET, not "".
325+
delete process.env.XDG_DATA_HOME;
326+
// biome-ignore lint/performance/noDelete: must be UNSET, not "".
327+
delete process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
328+
try {
329+
const { dbPath } = resolveDatabasePath();
330+
expect(dbPath.startsWith(realStorageRoot)).toBe(false);
331+
expect(dbPath.includes("mc-test-db-backstop-")).toBe(true);
332+
} finally {
333+
if (savedXdg !== undefined) process.env.XDG_DATA_HOME = savedXdg;
334+
if (savedTestDir !== undefined)
335+
process.env.MAGIC_CONTEXT_TEST_DATA_DIR = savedTestDir;
336+
}
337+
});
338+
339+
it("#when a test sets its own XDG_DATA_HOME #then that controlled dir is honored", () => {
340+
const dataHome = useTempDataHome("storage-db-backstop-xdg-");
341+
const { dbPath } = resolveDatabasePath();
342+
expect(dbPath).toBe(resolveDbPath(dataHome));
343+
});
344+
345+
it("#then every test package wires the isolation preload (root + plugin + pi-plugin + cli)", () => {
346+
// Structural guard: a new test package that forgets its bunfig
347+
// `[test] preload` is the exact hole that caused both incidents.
348+
const repoRoot = join(__dirname, "..", "..", "..", "..", "..");
349+
const bunfigs = [
350+
"bunfig.toml",
351+
"packages/plugin/bunfig.toml",
352+
"packages/pi-plugin/bunfig.toml",
353+
"packages/cli/bunfig.toml",
354+
];
355+
for (const rel of bunfigs) {
356+
const full = join(repoRoot, rel);
357+
expect(existsSync(full)).toBe(true);
358+
const body = readFileSync(full, "utf8");
359+
expect(body.includes("[test]")).toBe(true);
360+
expect(body.includes("preload")).toBe(true);
361+
expect(body.includes("test-preload.ts")).toBe(true);
362+
}
363+
});
364+
});
305365
});

packages/plugin/src/features/magic-context/storage-db.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { copyFileSync, cpSync, existsSync, mkdirSync } from "node:fs";
1+
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync } from "node:fs";
2+
import { tmpdir } from "node:os";
23
import { dirname, join } from "node:path";
34
import {
45
getLegacyOpenCodeMagicContextStorageDir,
@@ -44,7 +45,9 @@ export interface OpenDatabaseOptions {
4445
latestSupportedVersion?: number;
4546
}
4647

47-
function resolveDatabasePath(dbPathOverride?: string): { dbDir: string; dbPath: string } {
48+
// Exported for the test-isolation guard test. Returns a PATH only — opens no DB —
49+
// so a regression assertion is safe even if the resolution is wrong.
50+
export function resolveDatabasePath(dbPathOverride?: string): { dbDir: string; dbPath: string } {
4851
if (dbPathOverride) {
4952
return { dbDir: dirname(dbPathOverride), dbPath: dbPathOverride };
5053
}
@@ -67,10 +70,60 @@ function resolveDatabasePath(dbPathOverride?: string): { dbDir: string; dbPath:
6770
const dbDir = join(testDataDir, "cortexkit", "magic-context");
6871
return { dbDir, dbPath: join(dbDir, "context.db") };
6972
}
73+
// CWD-INDEPENDENT TEST BACKSTOP. The MAGIC_CONTEXT_TEST_DATA_DIR / XDG guard
74+
// above only fires when the bunfig `[test] preload` ran — which depends on
75+
// `bun test`'s CWD having a bunfig with `[test] preload`. A `bun test` from a
76+
// dir WITHOUT that wiring (monorepo root, a package missing its bunfig, or a
77+
// brand-new package) recursively runs every *.test.ts with NO preload, so a
78+
// bare openDatabase() would resolve to the user's REAL shared DB and run
79+
// migrations on it. That is exactly how the live DB was migrated to v41 by a
80+
// worktree whose LATEST was 41 (a re-run of the 2026-06-01 v26 incident).
81+
//
82+
// Bun sets NODE_ENV=test for EVERY `bun test` regardless of CWD/bunfig (and
83+
// it is never "test" in the plugin runtime — production never sets it). So if
84+
// we are under the test runner with neither the test data dir nor an explicit
85+
// override, we MUST NOT touch real storage: redirect into a throwaway temp dir
86+
// so the live DB is physically unreachable. This makes it structurally
87+
// impossible for ANY test, from ANY CWD, to read or migrate production data.
88+
// Fire ONLY when XDG_DATA_HOME is unset: that is the dangerous window where
89+
// getMagicContextStorageDir() below would otherwise resolve to the REAL
90+
// ~/.local/share shared DB. When a test sets its own XDG_DATA_HOME (a
91+
// per-test temp dir, e.g. to exercise path fallbacks or share a DB across
92+
// helper calls), getMagicContextStorageDir() already points inside that
93+
// controlled dir — honor it, do not override.
94+
if (process.env.NODE_ENV === "test" && !process.env.XDG_DATA_HOME) {
95+
// Memoized per-process so repeated openDatabase() calls in the same
96+
// unisolated test resolve to the SAME path (openDatabase caches by path;
97+
// a fresh temp dir per call would defeat the cache and hand back
98+
// different DB handles).
99+
const dbDir = getTestBackstopDbDir();
100+
if (!testBackstopWarned) {
101+
testBackstopWarned = true;
102+
log(
103+
"[magic-context] TEST BACKSTOP: NODE_ENV=test with no MAGIC_CONTEXT_TEST_DATA_DIR " +
104+
`— redirecting DB to a throwaway temp dir (${dbDir}) so no test can touch the ` +
105+
"user's real shared database. Wire `[test] preload` in this package's bunfig.toml.",
106+
);
107+
}
108+
return { dbDir, dbPath: join(dbDir, "context.db") };
109+
}
70110
const dbDir = getMagicContextStorageDir();
71111
return { dbDir, dbPath: join(dbDir, "context.db") };
72112
}
73113

114+
let testBackstopDbDir: string | null = null;
115+
let testBackstopWarned = false;
116+
function getTestBackstopDbDir(): string {
117+
if (!testBackstopDbDir) {
118+
testBackstopDbDir = join(
119+
mkdtempSync(join(tmpdir(), "mc-test-db-backstop-")),
120+
"cortexkit",
121+
"magic-context",
122+
);
123+
}
124+
return testBackstopDbDir;
125+
}
126+
74127
export function getDatabasePath(db: Database): string | null {
75128
return pathByDatabase.get(db) ?? null;
76129
}

0 commit comments

Comments
 (0)