From 586d77a3599a0145a5e6628f9a7d8f0966197f6e Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 07:31:29 +0800 Subject: [PATCH] test(memory): isolate the global config dir per test file (CI fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second dev-CI failure investigation (run 31648866555) with the identity-fixture fix in place: the same assertion still failed, still with no visible warning. Verified root cause: bun test runs all files in ONE process, sequentially, sharing one XDG_CONFIG_HOME. A file that runs before this one and triggers global-memory initialization (memory.test prepare, or instance boot via bootstrap.ts memory.init) leaves a VALID global memory.jsonc whose model this file's fake provider does not know. writeGlobal then silently no-ops over the valid file (config.ts returns false), configuration() loads the foreign model, resolveModel fails (the warning is captured by TestConsole and never reaches CI logs), and search fails closed with "unavailable". R1-00's identical flow passes because R1-07's finalizer removes the global file in between. CI-only because bun's file order is deterministic per filesystem state and the fresh CI checkout orders a contaminator before this file. Fix: pin a private OPENCODE_CONFIG_DIR per file (beforeAll/afterAll, live env getter — globalConfigDir reads it at call time) so the global file can never be contaminated by earlier files, plus a tripwire assertion after writeGlobal that the loaded global config carries the expected model. Registered separately (product hardening): writeGlobal should log when it declines to overwrite an existing valid config. Co-Authored-By: Claude --- .../memory/memory-global-identity.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/memory/memory-global-identity.test.ts b/packages/opencode/test/memory/memory-global-identity.test.ts index e62e9fb650..ad94ce4a24 100644 --- a/packages/opencode/test/memory/memory-global-identity.test.ts +++ b/packages/opencode/test/memory/memory-global-identity.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from "bun:test" +import { afterAll, beforeAll, describe, expect } from "bun:test" import { Database } from "@opencode-ai/core/database/database" import { ProjectTable } from "@opencode-ai/core/project/sql" import { eq } from "drizzle-orm" @@ -12,6 +12,7 @@ import { Effect, Layer } from "effect" import { stringify } from "yaml" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import fs from "node:fs" +import os from "node:os" import path from "node:path" import { Config } from "@/config/config" import { Git } from "@/git" @@ -31,6 +32,24 @@ import { InstanceRef } from "@/effect/instance-ref" import { provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" +// bun test runs all files in one process, sequentially, sharing one +// XDG_CONFIG_HOME — so a test file that runs before this one and triggers +// global-memory initialization leaves a VALID memory.jsonc whose model this +// file's fake provider does not know; writeGlobal then silently no-ops over +// it and search fails closed with "unavailable" (dev CI, deterministic). +// Pin a private config dir per file so the global file can never be +// contaminated by earlier files. +const pinnedConfigDir = path.join(os.tmpdir(), `opencode-memory-global-identity-${process.pid}`) +const previousConfigDir = process.env.OPENCODE_CONFIG_DIR +beforeAll(() => { + fs.mkdirSync(pinnedConfigDir, { recursive: true }) + process.env.OPENCODE_CONFIG_DIR = pinnedConfigDir +}) +afterAll(() => { + if (previousConfigDir === undefined) delete process.env.OPENCODE_CONFIG_DIR + else process.env.OPENCODE_CONFIG_DIR = previousConfigDir +}) + const now = "2026-08-12T12:00:00Z" const providerID = ProviderV2.ID.make("test") const enabledModel = ProviderTest.model({ providerID, id: ModelV2.ID.make("memory-on") }) @@ -166,6 +185,10 @@ describe("MEM-PR01-R1-03: memory is inert once the identity row is retired", () yield* project.setInitialized(info.id) yield* configStore.writeGlobal(baseConfig) + // Tripwire: writeGlobal silently no-ops over a pre-existing VALID + // config, so a contaminated global dir would leave a foreign model + // here and every search would fail closed. + expect((yield* configStore.loadGlobal())?.config.model).toBe("test/memory-on") const sessionID = SessionID.make("ses_retired_identity") const active = yield* memory.search({ sessionID, messages: [userMessage(sessionID)], query: "任意查询" })