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
3 changes: 2 additions & 1 deletion packages/opencode/src/file/ripgrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export namespace Ripgrep {
"x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
"x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
Comment thread
Astro-Han marked this conversation as resolved.
"arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" },
"ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" },
Comment thread
Astro-Han marked this conversation as resolved.
"x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
} as const

Expand Down Expand Up @@ -158,7 +159,7 @@ export namespace Ripgrep {
const config = PLATFORM[platformKey]
if (!config) throw new UnsupportedPlatformError({ platform: platformKey })

Comment thread
Astro-Han marked this conversation as resolved.
const version = "14.1.1"
const version = "15.1.0"
Comment thread
Astro-Han marked this conversation as resolved.
Comment thread
Astro-Han marked this conversation as resolved.
const filename = `ripgrep-${version}-${config.platform}.${config.extension}`
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`

Expand Down
13 changes: 12 additions & 1 deletion packages/opencode/src/npm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import path from "path"
import { readdir, rm } from "fs/promises"
import { Filesystem } from "@/util/filesystem"
import { Flock } from "@/util/flock"
import { Arborist } from "@npmcli/arborist"

export namespace Npm {
const log = Log.create({ service: "npm" })
Expand All @@ -29,6 +28,16 @@ export namespace Npm {
return path.join(Global.Path.cache, "packages", sanitize(pkg))
}

async function loadArborist() {
if (process.platform === "win32") {
// Bun on Windows does not support the UV_FS_O_FILEMAP flag used by tar
// for small files. tar snapshots this env var during module init, so set
// it immediately before Arborist can import tar.
process.env.__FAKE_PLATFORM__ = "linux"
}
return import("@npmcli/arborist")
}

function resolveEntryPoint(name: string, dir: string) {
let entrypoint: string | undefined
try {
Expand Down Expand Up @@ -68,6 +77,7 @@ export namespace Npm {
pkg,
})

const { Arborist } = await loadArborist()
const arborist = new Arborist({
path: dir,
binLinks: true,
Expand Down Expand Up @@ -108,6 +118,7 @@ export namespace Npm {
log.info("checking dependencies", { dir })

const reify = async () => {
const { Arborist } = await loadArborist()
const arb = new Arborist({
path: dir,
binLinks: true,
Expand Down
20 changes: 14 additions & 6 deletions packages/opencode/src/worktree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,14 +358,22 @@ export namespace Worktree {
}

function cleanDirectory(target: string) {
return Effect.promise(() =>
import("fs/promises")
.then((fsp) => fsp.rm(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }))
.catch((error) => {
// Match test preload cleanup: Windows can keep git and sqlite handles
Comment thread
Astro-Han marked this conversation as resolved.
// alive briefly after process teardown, so EBUSY needs a longer budget.
const maxRetries = process.platform === "win32" ? 30 : 5
Comment thread
Astro-Han marked this conversation as resolved.
const remove = (remaining: number): Effect.Effect<void> =>
fs.remove(target, { recursive: true, force: true }).pipe(
Effect.catch((error) => {
if (remaining > 0) {
return Effect.sleep(100).pipe(Effect.flatMap(() => remove(remaining - 1)))
}
const message = errorMessage(error)
throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
return Effect.sync(() => {
throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
})
}),
)
)
return remove(maxRetries)
}

const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/test/effect/cross-spawn-spawner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ describe("cross-spawn spawner", () => {
'process.stderr.write("stderr\\n", done)',
].join("\n"),
)
const [stdout, stderr] = yield* Effect.all([decodeByteStream(handle.stdout), decodeByteStream(handle.stderr)])
// Drain both pipes in parallel so Windows CI does not stall when stderr
// waits behind stdout decoding.
const [stdout, stderr] = yield* Effect.all(
Comment thread
Astro-Han marked this conversation as resolved.
Comment thread
Astro-Han marked this conversation as resolved.
[decodeByteStream(handle.stdout), decodeByteStream(handle.stderr)],
{ concurrency: 2 },
)
expect(stdout).toBe("stdout")
expect(stderr).toBe("stderr")
}),
Expand Down
62 changes: 43 additions & 19 deletions packages/opencode/test/global/runtime-namespace.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { tmpdir } from "../fixture/fixture"

function readGlobalPath(namespace?: string) {
type Roots = {
data: string
cache: string
config: string
state: string
}

function rootsFor(dir: string): Roots {
return {
data: path.join(dir, "share"),
cache: path.join(dir, "cache"),
config: path.join(dir, "config"),
state: path.join(dir, "state"),
}
}

function readGlobalPath(roots: Roots, namespace?: string) {
const script = `
process.env.XDG_DATA_HOME = "/tmp/pawwork-runtime-test/share"
process.env.XDG_CACHE_HOME = "/tmp/pawwork-runtime-test/cache"
process.env.XDG_CONFIG_HOME = "/tmp/pawwork-runtime-test/config"
process.env.XDG_STATE_HOME = "/tmp/pawwork-runtime-test/state"
process.env.XDG_DATA_HOME = ${JSON.stringify(roots.data)}
process.env.XDG_CACHE_HOME = ${JSON.stringify(roots.cache)}
process.env.XDG_CONFIG_HOME = ${JSON.stringify(roots.config)}
process.env.XDG_STATE_HOME = ${JSON.stringify(roots.state)}
if (${JSON.stringify(namespace)} !== undefined) {
process.env.PAWWORK_RUNTIME_NAMESPACE = ${JSON.stringify(namespace)}
} else {
delete process.env.PAWWORK_RUNTIME_NAMESPACE
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const { Global } = await import("./src/global/index.ts")
console.log(JSON.stringify(Global.Path))
Expand All @@ -25,24 +44,29 @@ function readGlobalPath(namespace?: string) {
return JSON.parse(Buffer.from(result.stdout).toString()) as Record<string, string>
}

function expectCoreRoots(paths: Record<string, string>, roots: Roots, namespace: string) {
expect(paths.data).toBe(path.join(roots.data, namespace))
expect(paths.cache).toBe(path.join(roots.cache, namespace))
expect(paths.config).toBe(path.join(roots.config, namespace))
expect(paths.state).toBe(path.join(roots.state, namespace))
}

describe("Global runtime namespace", () => {
test("defaults to OpenCode namespace outside PawWork desktop", () => {
const paths = readGlobalPath()
test("defaults to OpenCode namespace outside PawWork desktop", async () => {
await using tmp = await tmpdir()
const roots = rootsFor(tmp.path)
const paths = readGlobalPath(roots)

expect(paths.data).toBe("/tmp/pawwork-runtime-test/share/opencode")
expect(paths.cache).toBe("/tmp/pawwork-runtime-test/cache/opencode")
expect(paths.config).toBe("/tmp/pawwork-runtime-test/config/opencode")
expect(paths.state).toBe("/tmp/pawwork-runtime-test/state/opencode")
expectCoreRoots(paths, roots, "opencode")
})

test("uses PawWork namespace when enabled", () => {
const paths = readGlobalPath("pawwork")
test("uses PawWork namespace when enabled", async () => {
await using tmp = await tmpdir()
const roots = rootsFor(tmp.path)
const paths = readGlobalPath(roots, "pawwork")

expect(paths.data).toBe("/tmp/pawwork-runtime-test/share/pawwork")
expect(paths.cache).toBe("/tmp/pawwork-runtime-test/cache/pawwork")
expect(paths.config).toBe("/tmp/pawwork-runtime-test/config/pawwork")
expect(paths.state).toBe("/tmp/pawwork-runtime-test/state/pawwork")
expect(paths.bin).toBe("/tmp/pawwork-runtime-test/cache/pawwork/bin")
expect(paths.log).toBe("/tmp/pawwork-runtime-test/share/pawwork/log")
expectCoreRoots(paths, roots, "pawwork")
expect(paths.bin).toBe(path.join(roots.cache, "pawwork", "bin"))
expect(paths.log).toBe(path.join(roots.data, "pawwork", "log"))
})
})
28 changes: 24 additions & 4 deletions packages/opencode/test/plugin/workspace-adaptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ async function waitFor(fn: () => boolean, timeout = 5_000) {
throw new Error("timed out waiting for workspace status")
}

async function waitForCounter(file: string, min: number) {
Comment thread
Astro-Han marked this conversation as resolved.
const read = async () => {
const text = await Bun.file(file)
.text()
.catch((error) => {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "0"
throw error
})
const value = Number(text)
if (!Number.isFinite(value)) throw new Error(`invalid retry counter value: ${text}`)
return value
}
const end = Date.now() + 5_000
let value = 0
while (Date.now() < end) {
value = await read()
if (value > min) return value
await wait(50)
Comment thread
Astro-Han marked this conversation as resolved.
}
Comment thread
Astro-Han marked this conversation as resolved.
throw new Error(`timed out waiting for counter ${file} to exceed ${min}; last value=${value}`)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function pluginProject() {
return tmpdir({
git: true,
Expand Down Expand Up @@ -362,13 +384,11 @@ describe("plugin.workspace", () => {
await Instance.disposeAll()

await Workspace.get(workspace.id)
await wait(100)
const first = Number(await Bun.file(tmp.extra.counter).text())
const first = await waitForCounter(tmp.extra.counter, 0)
expect(first).toBeGreaterThan(0)

await Workspace.get(workspace.id)
await wait(100)
const second = Number(await Bun.file(tmp.extra.counter).text())
const second = await waitForCounter(tmp.extra.counter, first)
expect(second).toBeGreaterThan(first)

const status = Workspace.status().find((item) => item.workspaceID === workspace.id)
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/test/session/prompt-effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,8 @@ unix(
30_000,
)

const shellQueueTimeout = process.platform === "win32" ? 10_000 : 3_000

it.live(
"loop waits while shell runs and starts after shell exits",
() =>
Expand Down Expand Up @@ -1271,7 +1273,7 @@ it.live(
}),
Comment thread
Astro-Han marked this conversation as resolved.
{ git: true, config: providerCfg },
),
3_000,
shellQueueTimeout,
)

it.live(
Expand Down Expand Up @@ -1311,7 +1313,7 @@ it.live(
}),
{ git: true, config: providerCfg },
),
3_000,
shellQueueTimeout,
)

unix(
Expand Down
Loading