mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-02 07:03:45 +00:00
fix+refactor(vcs): fix HEAD filter bug and effectify VcsService (#17829)
This commit is contained in:
@@ -2,10 +2,10 @@ import { $ } from "bun"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ConfigProvider, Deferred, Effect, Fiber, Layer, ManagedRuntime, Option } from "effect"
|
||||
import { Deferred, Effect, Fiber, Option } from "effect"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { watcherConfigLayer, withServices } from "../fixture/instance"
|
||||
import { FileWatcher, FileWatcherService } from "../../src/file/watcher"
|
||||
import { InstanceContext } from "../../src/effect/instances"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
|
||||
@@ -16,35 +16,21 @@ const describeWatcher = FileWatcher.hasNativeBinding() && !process.env.CI ? desc
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const configLayer = ConfigProvider.layer(
|
||||
ConfigProvider.fromUnknown({
|
||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
|
||||
}),
|
||||
)
|
||||
|
||||
type BusUpdate = { directory?: string; payload: { type: string; properties: WatcherEvent } }
|
||||
type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
|
||||
|
||||
/** Run `body` with a live FileWatcherService. Runtime is acquired/released via Effect.scoped. */
|
||||
/** Run `body` with a live FileWatcherService. */
|
||||
function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
|
||||
return Instance.provide({
|
||||
return withServices(
|
||||
directory,
|
||||
fn: () =>
|
||||
Effect.gen(function* () {
|
||||
const ctx = Layer.sync(InstanceContext, () =>
|
||||
InstanceContext.of({ directory: Instance.directory, project: Instance.project }),
|
||||
)
|
||||
const layer = Layer.fresh(FileWatcherService.layer).pipe(Layer.provide(ctx), Layer.provide(configLayer))
|
||||
const rt = yield* Effect.acquireRelease(
|
||||
Effect.sync(() => ManagedRuntime.make(layer)),
|
||||
(rt) => Effect.promise(() => rt.dispose()),
|
||||
)
|
||||
yield* Effect.promise(() => rt.runPromise(FileWatcherService.use((s) => s.init())))
|
||||
yield* ready(directory)
|
||||
yield* body
|
||||
}).pipe(Effect.scoped, Effect.runPromise),
|
||||
})
|
||||
FileWatcherService.layer,
|
||||
async (rt) => {
|
||||
await rt.runPromise(FileWatcherService.use((s) => s.init()))
|
||||
await Effect.runPromise(ready(directory))
|
||||
await Effect.runPromise(body)
|
||||
},
|
||||
{ provide: [watcherConfigLayer] },
|
||||
)
|
||||
}
|
||||
|
||||
function listen(directory: string, check: (evt: WatcherEvent) => boolean, hit: (evt: WatcherEvent) => void) {
|
||||
|
||||
47
packages/opencode/test/fixture/instance.ts
Normal file
47
packages/opencode/test/fixture/instance.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ConfigProvider, Layer, ManagedRuntime } from "effect"
|
||||
import { InstanceContext } from "../../src/effect/instance-context"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
/** ConfigProvider that enables the experimental file watcher. */
|
||||
export const watcherConfigLayer = ConfigProvider.layer(
|
||||
ConfigProvider.fromUnknown({
|
||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Boot an Instance with the given service layers and run `body` with
|
||||
* the ManagedRuntime. Cleanup is automatic — the runtime is disposed
|
||||
* and Instance context is torn down when `body` completes.
|
||||
*
|
||||
* Layers may depend on InstanceContext (provided automatically).
|
||||
* Pass extra layers via `options.provide` (e.g. ConfigProvider.layer).
|
||||
*/
|
||||
export function withServices<S>(
|
||||
directory: string,
|
||||
layer: Layer.Layer<S, any, InstanceContext>,
|
||||
body: (rt: ManagedRuntime.ManagedRuntime<S, never>) => Promise<void>,
|
||||
options?: { provide?: Layer.Layer<never>[] },
|
||||
) {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
const ctx = Layer.sync(InstanceContext, () =>
|
||||
InstanceContext.of({ directory: Instance.directory, project: Instance.project }),
|
||||
)
|
||||
let resolved: Layer.Layer<S> = Layer.fresh(layer).pipe(Layer.provide(ctx)) as any
|
||||
if (options?.provide) {
|
||||
for (const l of options.provide) {
|
||||
resolved = resolved.pipe(Layer.provide(l)) as any
|
||||
}
|
||||
}
|
||||
const rt = ManagedRuntime.make(resolved)
|
||||
try {
|
||||
await body(rt)
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
117
packages/opencode/test/project/vcs.test.ts
Normal file
117
packages/opencode/test/project/vcs.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { $ } from "bun"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Layer, ManagedRuntime } from "effect"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { watcherConfigLayer, withServices } from "../fixture/instance"
|
||||
import { FileWatcher, FileWatcherService } from "../../src/file/watcher"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { Vcs, VcsService } from "../../src/project/vcs"
|
||||
|
||||
// Skip in CI — native @parcel/watcher binding needed
|
||||
const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function withVcs(
|
||||
directory: string,
|
||||
body: (rt: ManagedRuntime.ManagedRuntime<FileWatcherService | VcsService, never>) => Promise<void>,
|
||||
) {
|
||||
return withServices(
|
||||
directory,
|
||||
Layer.merge(FileWatcherService.layer, VcsService.layer),
|
||||
async (rt) => {
|
||||
await rt.runPromise(FileWatcherService.use((s) => s.init()))
|
||||
await rt.runPromise(VcsService.use((s) => s.init()))
|
||||
await Bun.sleep(200)
|
||||
await body(rt)
|
||||
},
|
||||
{ provide: [watcherConfigLayer] },
|
||||
)
|
||||
}
|
||||
|
||||
type BranchEvent = { directory?: string; payload: { type: string; properties: { branch?: string } } }
|
||||
|
||||
/** Wait for a Vcs.Event.BranchUpdated event on GlobalBus */
|
||||
function nextBranchUpdate(directory: string, timeout = 5000) {
|
||||
return new Promise<string | undefined>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
GlobalBus.off("event", on)
|
||||
reject(new Error("timed out waiting for BranchUpdated event"))
|
||||
}, timeout)
|
||||
|
||||
function on(evt: BranchEvent) {
|
||||
if (evt.directory !== directory) return
|
||||
if (evt.payload.type !== Vcs.Event.BranchUpdated.type) return
|
||||
clearTimeout(timer)
|
||||
GlobalBus.off("event", on)
|
||||
resolve(evt.payload.properties.branch)
|
||||
}
|
||||
|
||||
GlobalBus.on("event", on)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describeVcs("Vcs", () => {
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
test("branch() returns current branch name", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await withVcs(tmp.path, async (rt) => {
|
||||
const branch = await rt.runPromise(VcsService.use((s) => s.branch()))
|
||||
expect(branch).toBeDefined()
|
||||
expect(typeof branch).toBe("string")
|
||||
})
|
||||
})
|
||||
|
||||
test("branch() returns undefined for non-git directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await withVcs(tmp.path, async (rt) => {
|
||||
const branch = await rt.runPromise(VcsService.use((s) => s.branch()))
|
||||
expect(branch).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
test("publishes BranchUpdated when .git/HEAD changes", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const branch = `test-${Math.random().toString(36).slice(2)}`
|
||||
await $`git branch ${branch}`.cwd(tmp.path).quiet()
|
||||
|
||||
await withVcs(tmp.path, async () => {
|
||||
const pending = nextBranchUpdate(tmp.path)
|
||||
|
||||
const head = path.join(tmp.path, ".git", "HEAD")
|
||||
await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
|
||||
|
||||
const updated = await pending
|
||||
expect(updated).toBe(branch)
|
||||
})
|
||||
})
|
||||
|
||||
test("branch() reflects the new branch after HEAD change", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const branch = `test-${Math.random().toString(36).slice(2)}`
|
||||
await $`git branch ${branch}`.cwd(tmp.path).quiet()
|
||||
|
||||
await withVcs(tmp.path, async (rt) => {
|
||||
const pending = nextBranchUpdate(tmp.path)
|
||||
|
||||
const head = path.join(tmp.path, ".git", "HEAD")
|
||||
await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
|
||||
|
||||
await pending
|
||||
const current = await rt.runPromise(VcsService.use((s) => s.branch()))
|
||||
expect(current).toBe(branch)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user