From ea8e55dcae06f274b185e6363b811952409f46fe Mon Sep 17 00:00:00 2001 From: fanhuanjie Date: Tue, 23 Jun 2026 23:34:16 +0800 Subject: [PATCH] fix(test): isolate tmpdir from parent git worktree in test fixtures Add outsideGit option to tmpdirScoped and withTmpdirOutsideGit helper to create tmpdirs outside the repo's git worktree, preventing tests from inheriting the parent repo's .git directory. Fix tests that require a non-git environment or explicit git init: - migrate-global.test.ts, project.test.ts: use withTmpdirOutsideGit - worktree.test.ts: pass outsideGit option to provideTmpdirInstance - lsp/index.test.ts: enable git for LSP spawn test - instruction.test.ts: enable git for project tmpdir - classify-integration.test.ts, length-tool-safety.test.ts: fix tool arg casing (filePath -> file_path) --- packages/opencode/test/fixture/fixture.ts | 30 ++++++++++- packages/opencode/test/lsp/index.test.ts | 2 +- .../test/project/migrate-global.test.ts | 52 ++++++++++--------- .../opencode/test/project/project.test.ts | 14 ++--- .../opencode/test/project/worktree.test.ts | 16 +++--- .../test/session/classify-integration.test.ts | 4 +- .../opencode/test/session/instruction.test.ts | 1 + .../test/session/length-tool-safety.test.ts | 10 ++-- 8 files changed, 83 insertions(+), 46 deletions(-) diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index bc03a4c40..e5e269adc 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -44,6 +44,21 @@ export async function cleanupTmpdir(dir: string, cleanup = clean) { }) } +function outsideGitTmpRoot() { + if (process.platform === "win32") return os.tmpdir() + return "/tmp" +} + +/** Tmpdirs under cwd inherit the parent repo's worktree; use this when tests need a non-git project. */ +export function withTmpdirOutsideGit(fn: () => Promise): Promise { + const prev = process.env["MIMOCODE_TEST_TMPDIR_ROOT"] + process.env["MIMOCODE_TEST_TMPDIR_ROOT"] = outsideGitTmpRoot() + return fn().finally(() => { + if (prev !== undefined) process.env["MIMOCODE_TEST_TMPDIR_ROOT"] = prev + else delete process.env["MIMOCODE_TEST_TMPDIR_ROOT"] + }) +} + async function stop(dir: string) { if (!(await exists(dir))) return await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow() @@ -95,8 +110,19 @@ export async function tmpdir(options?: TmpDirOptions) { } /** Effectful scoped tmpdir. Cleaned up when the scope closes. Make sure these stay in sync */ -export function tmpdirScoped(options?: { git?: boolean; config?: Partial }) { +export function tmpdirScoped(options?: { git?: boolean; config?: Partial; outsideGit?: boolean }) { return Effect.gen(function* () { + const prevRoot = options?.outsideGit ? process.env["MIMOCODE_TEST_TMPDIR_ROOT"] : undefined + if (options?.outsideGit) process.env["MIMOCODE_TEST_TMPDIR_ROOT"] = outsideGitTmpRoot() + if (options?.outsideGit) { + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (prevRoot !== undefined) process.env["MIMOCODE_TEST_TMPDIR_ROOT"] = prevRoot + else delete process.env["MIMOCODE_TEST_TMPDIR_ROOT"] + }), + ) + } + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const dirpath = sanitizePath( path.join(process.env["MIMOCODE_TEST_TMPDIR_ROOT"] ?? os.tmpdir(), "mimocode-test-" + Math.random().toString(36).slice(2)), @@ -150,7 +176,7 @@ export const provideInstance = export function provideTmpdirInstance( self: (path: string) => Effect.Effect, - options?: { git?: boolean; config?: Partial }, + options?: { git?: boolean; config?: Partial; outsideGit?: boolean }, ) { return Effect.gen(function* () { const path = yield* tmpdirScoped(options) diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index d138f56e3..fa6cdd2da 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -30,7 +30,7 @@ describe("lsp.spawn", () => { } }), ), - { config: { lsp: true } }, + { config: { lsp: true }, git: true }, ), ) diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index 46eacc0b0..25ee645cf 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -7,7 +7,7 @@ import { ProjectID } from "../../src/project/schema" import { SessionID } from "../../src/session/schema" import { Log } from "../../src/util" import { $ } from "bun" -import { tmpdir } from "../fixture/fixture" +import { tmpdir, withTmpdirOutsideGit } from "../fixture/fixture" import { Effect } from "effect" Log.init({ print: false }) @@ -61,30 +61,32 @@ function ensureGlobal() { } describe("migrateFromGlobal", () => { - test("migrates global sessions on first project creation", async () => { - // 1. Start in a non-git directory — fromDirectory yields the "global" project ID. - await using tmp = await tmpdir() - const { project: pre } = await run((svc) => svc.fromDirectory(tmp.path)) - expect(pre.id).toBe(ProjectID.global) - - // 2. Seed a session under "global" with matching directory - const id = uid() - seed({ id, dir: tmp.path, project: ProjectID.global }) - - // 3. Initialise git so the project gets a real (UUID) ID - await $`git init`.cwd(tmp.path).quiet() - await $`git config user.name "Test"`.cwd(tmp.path).quiet() - await $`git config user.email "test@opencode.test"`.cwd(tmp.path).quiet() - await $`git config commit.gpgsign false`.cwd(tmp.path).quiet() - - const { project: real } = await run((svc) => svc.fromDirectory(tmp.path)) - expect(real.id).not.toBe(ProjectID.global) - - // 4. The session should have been migrated to the real project ID - const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) - expect(row).toBeDefined() - expect(row!.project_id).toBe(real.id) - }) + test("migrates global sessions on first project creation", () => + withTmpdirOutsideGit(async () => { + // 1. Start in a non-git directory — fromDirectory yields the "global" project ID. + await using tmp = await tmpdir() + const { project: pre } = await run((svc) => svc.fromDirectory(tmp.path)) + expect(pre.id).toBe(ProjectID.global) + + // 2. Seed a session under "global" with matching directory + const id = uid() + seed({ id, dir: tmp.path, project: ProjectID.global }) + + // 3. Initialise git so the project gets a real (UUID) ID + await $`git init`.cwd(tmp.path).quiet() + await $`git config user.name "Test"`.cwd(tmp.path).quiet() + await $`git config user.email "test@opencode.test"`.cwd(tmp.path).quiet() + await $`git config commit.gpgsign false`.cwd(tmp.path).quiet() + + const { project: real } = await run((svc) => svc.fromDirectory(tmp.path)) + expect(real.id).not.toBe(ProjectID.global) + + // 4. The session should have been migrated to the real project ID + const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) + expect(row).toBeDefined() + expect(row!.project_id).toBe(real.id) + }), + ) test("migrates global sessions even when project row already exists", async () => { // 1. Create a repo with a commit — real project ID created immediately diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index f6c1311e3..ca69e37eb 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -3,7 +3,7 @@ import { Project } from "../../src/project" import { Log } from "../../src/util" import { $ } from "bun" import path from "path" -import { tmpdir } from "../fixture/fixture" +import { tmpdir, withTmpdirOutsideGit } from "../fixture/fixture" import { GlobalBus } from "../../src/bus/global" import { ProjectID } from "../../src/project/schema" import { Effect, Layer, Stream } from "effect" @@ -103,11 +103,13 @@ describe("Project.fromDirectory", () => { expect(await Bun.file(idFile).exists()).toBe(true) }) - test("returns global for non-git directory", async () => { - await using tmp = await tmpdir() - const { project } = await run((svc) => svc.fromDirectory(tmp.path)) - expect(project.id).toBe(ProjectID.global) - }) + test("returns global for non-git directory", () => + withTmpdirOutsideGit(async () => { + await using tmp = await tmpdir() + const { project } = await run((svc) => svc.fromDirectory(tmp.path)) + expect(project.id).toBe(ProjectID.global) + }), + ) test("disables vcs when .git is anchored at $HOME", async () => { await using tmp = await tmpdir() diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index 252e7c32a..3621a80ff 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -84,14 +84,16 @@ describe("Worktree", () => { ) it.live("throws NotGitError for non-git directories", () => - provideTmpdirInstance(() => - Effect.gen(function* () { - const svc = yield* Worktree.Service - const exit = yield* Effect.exit(svc.makeWorktreeInfo()) + provideTmpdirInstance( + () => + Effect.gen(function* () { + const svc = yield* Worktree.Service + const exit = yield* Effect.exit(svc.makeWorktreeInfo()) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError) - }), + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError) + }), + { outsideGit: true }, ), ) }) diff --git a/packages/opencode/test/session/classify-integration.test.ts b/packages/opencode/test/session/classify-integration.test.ts index 8b0383d60..57f349bfa 100644 --- a/packages/opencode/test/session/classify-integration.test.ts +++ b/packages/opencode/test/session/classify-integration.test.ts @@ -87,7 +87,7 @@ describe("classifier routing — integration", () => { await using tmp = await tmpdir({ git: true }) const readmePath = path.join(tmp.path, "README.md") const stub = startScriptedLLMServer([ - { lines: toolCallResponse({ id: "call_0", name: "read", args: JSON.stringify({ filePath: readmePath }) }) }, + { lines: toolCallResponse({ id: "call_0", name: "read", args: JSON.stringify({ file_path: readmePath }) }) }, { lines: textStopResponse("done.") }, ]) try { @@ -156,7 +156,7 @@ describe("classifier routing — integration", () => { const stub = startScriptedLLMServer([ // Pending client tool part + finish_reason "stop": must continue despite // json_schema mode, instead of breaking with StructuredOutputError. - { lines: toolCallStopResponse({ id: "call_0", name: "read", args: JSON.stringify({ filePath: readmePath }) }) }, + { lines: toolCallStopResponse({ id: "call_0", name: "read", args: JSON.stringify({ file_path: readmePath }) }) }, { lines: textStopResponse("plain text terminates the loop") }, ]) try { diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index b5bba50cf..46dc29fb3 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -230,6 +230,7 @@ describe("Instruction.system", () => { }, }) await using projectTmp = await tmpdir({ + git: true, init: async (dir) => { await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions") }, diff --git a/packages/opencode/test/session/length-tool-safety.test.ts b/packages/opencode/test/session/length-tool-safety.test.ts index 70602e22f..f5977f158 100644 --- a/packages/opencode/test/session/length-tool-safety.test.ts +++ b/packages/opencode/test/session/length-tool-safety.test.ts @@ -38,7 +38,9 @@ function run(fx: Effect.Effect { - test("length finish with a complete client tool call does not inject an output-length continuation", async () => { + test( + "length finish with a complete client tool call does not inject an output-length continuation", + async () => { await using tmp = await tmpdir({ git: true }) const readmePath = path.join(tmp.path, "README.md") @@ -51,7 +53,7 @@ describe("length + tool safety contract", () => { lines: toolCallLengthResponse({ id: "call_0", name: "read", - args: JSON.stringify({ filePath: readmePath }), + args: JSON.stringify({ file_path: readmePath }), }), }, { lines: textStopResponse("done.") }, @@ -117,5 +119,7 @@ describe("length + tool safety contract", () => { } finally { await stub.stop() } - }) + }, + 30_000, + ) })