Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions packages/opencode/test/fixture/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(fn: () => Promise<T>): Promise<T> {
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()
Expand Down Expand Up @@ -95,8 +110,19 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
}

/** Effectful scoped tmpdir. Cleaned up when the scope closes. Make sure these stay in sync */
export function tmpdirScoped(options?: { git?: boolean; config?: Partial<Config.Info> }) {
export function tmpdirScoped(options?: { git?: boolean; config?: Partial<Config.Info>; 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)),
Expand Down Expand Up @@ -150,7 +176,7 @@ export const provideInstance =

export function provideTmpdirInstance<A, E, R>(
self: (path: string) => Effect.Effect<A, E, R>,
options?: { git?: boolean; config?: Partial<Config.Info> },
options?: { git?: boolean; config?: Partial<Config.Info>; outsideGit?: boolean },
) {
return Effect.gen(function* () {
const path = yield* tmpdirScoped(options)
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/lsp/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("lsp.spawn", () => {
}
}),
),
{ config: { lsp: true } },
{ config: { lsp: true }, git: true },
),
)

Expand Down
52 changes: 27 additions & 25 deletions packages/opencode/test/project/migrate-global.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 9 additions & 7 deletions packages/opencode/test/project/worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
),
)
})
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/test/session/classify-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/test/session/instruction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
},
Expand Down
10 changes: 7 additions & 3 deletions packages/opencode/test/session/length-tool-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ function run<A, E>(fx: Effect.Effect<A, E, SessionPrompt.Service | Session.Servi
}

describe("length + tool safety contract", () => {
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")
Expand All @@ -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.") },
Expand Down Expand Up @@ -117,5 +119,7 @@ describe("length + tool safety contract", () => {
} finally {
await stub.stop()
}
})
},
30_000,
)
})
Loading