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, + ) })