diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96cc78f8def6..146d1980e245 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,9 +84,12 @@ jobs: path: ~/.cache/opencode/bin key: opencode-bin-${{ runner.os }}-${{ hashFiles('packages/core/src/ripgrep/binary.ts') }} - name: Run unit tests - # opencode#test alone takes ~20 minutes on GitHub-hosted windows runners + # opencode#test alone takes ~20 minutes on GitHub-hosted windows runners. + # --log-order=stream keeps per-task output visible if the step is ever + # killed; turbo's CI default (grouped) buffers a task's output until it + # finishes, so a timeout eats the entire log (LAC-2717). timeout-minutes: 35 - run: GITHUB_ACTIONS=false bun turbo test + run: GITHUB_ACTIONS=false bun turbo test --log-order=stream env: OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} diff --git a/packages/core/test/lib/effect.ts b/packages/core/test/lib/effect.ts index 131ec5cc6bc2..c7df26384e86 100644 --- a/packages/core/test/lib/effect.ts +++ b/packages/core/test/lib/effect.ts @@ -19,24 +19,26 @@ const run = (value: Body, layer: Layer.Layer return yield* exit }).pipe(Effect.runPromise) -const make = (testLayer: Layer.Layer, liveLayer: Layer.Layer) => { +const make = (testLayer: Layer.Layer, liveLayer: Layer.Layer, defaults?: number | TestOptions) => { + const options = (opts?: number | TestOptions) => opts ?? defaults + const effect = (name: string, value: Body, opts?: number | TestOptions) => - test(name, () => run(value, testLayer), opts) + test(name, () => run(value, testLayer), options(opts)) effect.only = (name: string, value: Body, opts?: number | TestOptions) => - test.only(name, () => run(value, testLayer), opts) + test.only(name, () => run(value, testLayer), options(opts)) effect.skip = (name: string, value: Body, opts?: number | TestOptions) => - test.skip(name, () => run(value, testLayer), opts) + test.skip(name, () => run(value, testLayer), options(opts)) const live = (name: string, value: Body, opts?: number | TestOptions) => - test(name, () => run(value, liveLayer), opts) + test(name, () => run(value, liveLayer), options(opts)) live.only = (name: string, value: Body, opts?: number | TestOptions) => - test.only(name, () => run(value, liveLayer), opts) + test.only(name, () => run(value, liveLayer), options(opts)) live.skip = (name: string, value: Body, opts?: number | TestOptions) => - test.skip(name, () => run(value, liveLayer), opts) + test.skip(name, () => run(value, liveLayer), options(opts)) return { effect, live } } @@ -49,5 +51,5 @@ const liveEnv = TestConsole.layer export const it = make(testEnv, liveEnv) -export const testEffect = (layer: Layer.Layer) => - make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv)) +export const testEffect = (layer: Layer.Layer, defaults?: number | TestOptions) => + make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv), defaults) diff --git a/packages/core/test/project-copy.test.ts b/packages/core/test/project-copy.test.ts index d37f6302074a..15e24f4ffd13 100644 --- a/packages/core/test/project-copy.test.ts +++ b/packages/core/test/project-copy.test.ts @@ -19,6 +19,10 @@ import { testEffect } from "./lib/effect" const it = testEffect( AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, EventV2.node, ProjectDirectories.node])), + // These tests chain many git subprocess ops; on GitHub-hosted Windows + // runners each op costs 100ms-2.8s under full-suite load, exceeding bun's + // 5s default per-test timeout (LAC-2717). + { timeout: 30_000 }, ) function abs(input: string) { diff --git a/packages/core/test/repository-cache.test.ts b/packages/core/test/repository-cache.test.ts index 2dd0ce250288..198ad81bbc49 100644 --- a/packages/core/test/repository-cache.test.ts +++ b/packages/core/test/repository-cache.test.ts @@ -12,7 +12,10 @@ import { git, gitRemote } from "./fixture/git" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" -const it = testEffect(Layer.empty) +// These tests chain many git subprocess ops; on GitHub-hosted Windows runners +// each op costs 100ms-2.8s under full-suite load, exceeding bun's 5s default +// per-test timeout (LAC-2717). +const it = testEffect(Layer.empty, { timeout: 30_000 }) describe("RepositoryCache", () => { it.live("replaces a stale cache directory before cloning", () => diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts index 55e4ead7ed2e..7913361daa86 100644 --- a/packages/core/test/snapshot.test.ts +++ b/packages/core/test/snapshot.test.ts @@ -15,8 +15,13 @@ import { Hash } from "@opencode-ai/core/util/hash" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" +// These tests chain many git subprocess ops; on GitHub-hosted Windows +// runners each op costs 100ms-2.8s under full-suite load, exceeding bun's +// 5s default per-test timeout (LAC-2717). +const it = testEffect(Layer.empty, { timeout: 30_000 }) + describe("Snapshot", () => { - testEffect(Layer.empty).live("captures and restores Location-scoped changes", () => + it.live("captures and restores Location-scoped changes", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => @@ -70,7 +75,7 @@ describe("Snapshot", () => { ), ) - testEffect(Layer.empty).live("treats capture outside Git as unavailable", () => + it.live("treats capture outside Git as unavailable", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => @@ -86,7 +91,7 @@ describe("Snapshot", () => { ), ) - testEffect(Layer.empty).live("isolates snapshot indexes by canonical Git worktree", () => + it.live("isolates snapshot indexes by canonical Git worktree", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => @@ -134,7 +139,7 @@ describe("Snapshot", () => { ), ) - testEffect(Layer.empty).live("checks out a legacy revert snapshot without removing unrelated files", () => + it.live("checks out a legacy revert snapshot without removing unrelated files", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index ba7d2e25a964..91011122196d 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -1,7 +1,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { httpClient, path } from "@opencode-ai/core/effect/app-node-platform" import { NodePath } from "@effect/platform-node" -import { Effect, Layer, Path, Schema, Context } from "effect" +import { Effect, Layer, Path, Schedule, Schema, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -34,6 +34,14 @@ export const layer: Layer.Layer + process.platform === "win32" + ? fs.rename(from, to).pipe(Effect.retry({ times: 5, schedule: Schedule.spaced(100) })) + : fs.rename(from, to) + const download = Effect.fn("Discovery.download")(function* (url: string, dest: string) { if (yield* fs.exists(dest).pipe(Effect.orDie)) return true @@ -106,11 +114,11 @@ export const layer: Layer.Layer Effect.gen(function* () { - if (cached) yield* fs.rename(backup, root).pipe(Effect.ignore) + if (cached) yield* rename(backup, root).pipe(Effect.ignore) return yield* Effect.fail(error) }), ), diff --git a/turbo.json b/turbo.json index 5e93640b1fad..9d0696ea4b3e 100644 --- a/turbo.json +++ b/turbo.json @@ -15,7 +15,8 @@ }, "@opencode-ai/core#test": { "dependsOn": ["^build"], - "outputs": [] + "outputs": [], + "passThroughEnv": ["*"] }, "@opencode-ai/app#test": { "dependsOn": ["^build"],