Skip to content
Closed
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
29 changes: 24 additions & 5 deletions packages/opencode/test/fixture/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,29 @@ async function stop(dir: string) {
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
}

// Unauthenticated HTTP route tests must use a fixture under cwd because the
// server rejects directories outside the checkout. node_modules is gitignored,
// and git:true fixtures create their own .git so VCS detection stops there.
const cwdFixtureRoot = () => path.join(process.cwd(), "node_modules", ".mimocode-cwd-fixtures")

function tmpdirBase(root?: "cwd") {
if (root === "cwd") return cwdFixtureRoot()
return process.env["MIMOCODE_TEST_TMPDIR_ROOT"] ?? os.tmpdir()
}

type TmpDirOptions<T> = {
git?: boolean
outsideGit?: boolean
config?: Partial<Config.Info>
init?: (dir: string) => Promise<T>
dispose?: (dir: string) => Promise<T>
root?: "cwd"
}
export async function tmpdir<T>(options?: TmpDirOptions<T>) {
const prevRoot = options?.outsideGit ? process.env["MIMOCODE_TEST_TMPDIR_ROOT"] : undefined
if (options?.outsideGit) process.env["MIMOCODE_TEST_TMPDIR_ROOT"] = outsideGitTmpRoot()
const dirpath = sanitizePath(
path.join(process.env["MIMOCODE_TEST_TMPDIR_ROOT"] ?? os.tmpdir(), "mimocode-test-" + Math.random().toString(36).slice(2)),
path.join(tmpdirBase(options?.root), "mimocode-test-" + Math.random().toString(36).slice(2)),
)
await fs.mkdir(dirpath, { recursive: true })
if (options?.git) {
Expand Down Expand Up @@ -116,8 +127,15 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
return result
}

type ScopedTmpDirOptions = {
git?: boolean
config?: Partial<Config.Info>
outsideGit?: boolean
root?: "cwd"
}

/** 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>; outsideGit?: boolean }) {
export function tmpdirScoped(options?: ScopedTmpDirOptions) {
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()
Expand All @@ -132,7 +150,7 @@ export function tmpdirScoped(options?: { git?: boolean; config?: Partial<Config.

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)),
path.join(tmpdirBase(options?.root), "mimocode-test-" + Math.random().toString(36).slice(2)),
)
yield* Effect.promise(() => fs.mkdir(dirpath, { recursive: true }))
const dir = sanitizePath(yield* Effect.promise(() => fs.realpath(dirpath)))
Expand Down Expand Up @@ -183,7 +201,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>; outsideGit?: boolean },
options?: ScopedTmpDirOptions,
) {
return Effect.gen(function* () {
const path = yield* tmpdirScoped(options)
Expand All @@ -207,7 +225,7 @@ export function provideTmpdirInstance<A, E, R>(

export function provideTmpdirServer<A, E, R>(
self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,
options?: { git?: boolean; config?: (url: string) => Partial<Config.Info> },
options?: { git?: boolean; config?: (url: string) => Partial<Config.Info>; root?: "cwd" },
): Effect.Effect<
A,
E | PlatformError.PlatformError,
Expand All @@ -218,6 +236,7 @@ export function provideTmpdirServer<A, E, R>(
return yield* provideTmpdirInstance((dir) => self({ dir, llm }), {
git: options?.git,
config: options?.config?.(llm.url),
root: options?.root,
})
})
}
72 changes: 67 additions & 5 deletions packages/opencode/test/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,79 @@
// xdg-basedir reads env vars at import time, so we must set these first
import os from "os"
import path from "path"
import { constants as fsConstants } from "fs"
import fs from "fs/promises"
import { setTimeout as sleep } from "node:timers/promises"
import { afterAll } from "bun:test"

// Set XDG env vars FIRST, before any src/ imports
const dir = path.join(os.tmpdir(), "mimocode-test-data-" + process.pid)
const forbiddenFixtureRoots = ["/etc", "/proc", "/sys", "/dev", "/boot", "/private/etc"]

function containsPath(parent: string, child: string) {
const relative = path.relative(path.resolve(parent), path.resolve(child))
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
}

async function findGitRoot(directory: string): Promise<string | undefined> {
const current = path.resolve(directory)
const hasGitDir = await fs
.stat(path.join(current, ".git"))
.then(() => true)
.catch(() => false)
if (hasGitDir) return current
const parent = path.dirname(current)
if (parent === current) return undefined
return findGitRoot(parent)
}

async function gitFreeParent(directory: string): Promise<string> {
const root = await findGitRoot(directory)
if (!root) return path.resolve(directory)
const parent = path.dirname(root)
if (parent === root) return parent
return gitFreeParent(parent)
}

async function isWritableDirectory(directory: string) {
const isDirectory = await fs
.stat(directory)
.then((stat) => stat.isDirectory())
.catch(() => false)
if (!isDirectory) return false
return fs
.access(directory, fsConstants.W_OK)
.then(() => true)
.catch(() => false)
}

async function isFixtureBaseBlocked(candidate: string) {
const resolved = path.resolve(candidate)
if (resolved === path.parse(resolved).root) return true
if (await findGitRoot(resolved)) return true
if (forbiddenFixtureRoots.some((forbidden) => containsPath(forbidden, resolved))) return true
return !(await isWritableDirectory(resolved))
}

async function fixtureBase() {
const candidates = await Promise.all(
[os.tmpdir(), os.homedir(), await gitFreeParent(process.cwd())].map(async (candidate) => ({
candidate,
blocked: await isFixtureBaseBlocked(candidate),
})),
)
const selected = candidates.find((candidate) => !candidate.blocked)
return selected?.candidate ?? os.tmpdir()
}

// Set XDG env vars FIRST, before any src/ imports. Keep the process-wide data
// root outside the repo checkout and unsafe system roots because worktree
// bootstrap creates real project instances under Global.Path.data.
const base = await fixtureBase()
const dir = path.join(base, "mimocode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true })

// Route fixture tmpdirs under cwd so they pass the InstanceMiddleware cwd
// containment check (security: unauthenticated servers restrict directory to cwd subtree).
const fixtureRoot = path.join(process.cwd(), ".mimocode-test-fixtures-" + process.pid)
// Default fixture tmpdirs should not inherit the repository checkout's worktree.
// HTTP route tests that need cwd containment opt into root: "cwd".
const fixtureRoot = path.join(base, ".mimocode-test-fixtures-" + process.pid)
await fs.mkdir(fixtureRoot, { recursive: true })
process.env["MIMOCODE_TEST_TMPDIR_ROOT"] = fixtureRoot
afterAll(async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/server/project-init-git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe("project.initGit endpoint", () => {
})

test("does not reload when the project is already git", async () => {
await using tmp = await tmpdir({ git: true })
await using tmp = await tmpdir({ git: true, root: "cwd" })
const app = Server.Default().app
const seen: { directory?: string; payload: { type: string } }[] = []
const fn = (evt: { directory?: string; payload: { type: string } }) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/test/server/session-prompt-busy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("ErrorMiddleware → BusyError mapping", () => {

describe("POST /session/:sessionID/message busy-runner behavior", () => {
test("returns 409 when session main runner is already busy", async () => {
await using tmp = await tmpdir({})
await using tmp = await tmpdir({ git: true, root: "cwd" })

const status = await Instance.provide({
directory: tmp.path,
Expand Down Expand Up @@ -84,7 +84,7 @@ describe("POST /session/:sessionID/message busy-runner behavior", () => {
})

test("POST /:sessionID/abort frees runner; subsequent POST is no longer rejected with 409", async () => {
await using tmp = await tmpdir({})
await using tmp = await tmpdir({ git: true, root: "cwd" })

const result = await Instance.provide({
directory: tmp.path,
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/test/server/workflows-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ describe("workflows routes — live runtime", () => {
resumed: false,
})
}),
{ git: true, config: providerCfg },
{ git: true, config: providerCfg, root: "cwd" },
),
)

Expand Down Expand Up @@ -269,7 +269,7 @@ describe("workflows routes — live runtime", () => {
expect(row!.status).toBe("completed")
expect(row!.succeeded).toBeGreaterThanOrEqual(1)
}),
{ git: true, config: providerCfg },
{ git: true, config: providerCfg, root: "cwd" },
),
// Headroom over the default 5s: this exercises a full Instance bootstrap +
// a real runtime run + an HTTP round-trip, and runs alongside the heavy
Expand Down
Loading