mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-27 22:10:11 -04:00
118 lines
5.5 KiB
TypeScript
118 lines
5.5 KiB
TypeScript
import { $ } from "bun"
|
|
import { describe, expect } from "bun:test"
|
|
import fs from "fs/promises"
|
|
import path from "path"
|
|
import { Effect } from "effect"
|
|
import { Agent } from "@opencode-ai/core/agent"
|
|
import { Bus } from "@opencode-ai/core/bus"
|
|
import { Database } from "@opencode-ai/core/database/database"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
|
import { Model } from "@opencode-ai/core/model"
|
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
|
import { Provider } from "@opencode-ai/core/provider"
|
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
import { Session } from "@opencode-ai/core/session"
|
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
|
import { Snapshot } from "@opencode-ai/core/snapshot"
|
|
import { Money } from "@opencode-ai/schema/money"
|
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
|
import { Global } from "@opencode-ai/util/global"
|
|
import { tempGlobalLayer } from "./fixture/global"
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
import { testEffect } from "./lib/effect"
|
|
|
|
const it = testEffect(
|
|
AppNodeBuilder.build(
|
|
LayerNode.group([Database.node, Bus.node, SessionProjector.node, Session.node, LocationServiceMap.node]),
|
|
[
|
|
[Bus.node, Bus.configured({ persist: true })],
|
|
[Global.node, tempGlobalLayer],
|
|
[SessionExecution.node, SessionExecution.noopLayer],
|
|
],
|
|
),
|
|
)
|
|
|
|
describe("Session.revert files", () => {
|
|
it.live("undoes and restores a file rename without losing either path", () =>
|
|
Effect.gen(function* () {
|
|
const tmp = yield* Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
)
|
|
const directory = path.join(tmp.path, "project")
|
|
const original = path.join(directory, "old name.txt")
|
|
const renamed = path.join(directory, "new name.txt")
|
|
yield* Effect.promise(async () => {
|
|
await fs.mkdir(directory)
|
|
await Bun.write(original, "Preserve this content.\n")
|
|
await Bun.write(path.join(directory, "unrelated.txt"), "Unrelated content.\n")
|
|
await $`git init -q`.cwd(directory).quiet()
|
|
await $`git -c core.fsmonitor=false add .`.cwd(directory).quiet()
|
|
})
|
|
|
|
const session = yield* Session.Service
|
|
const database = yield* Database.Service
|
|
const bus = yield* Bus.Service
|
|
const created = yield* session.create({ location: { directory: AbsolutePath.make(directory) } })
|
|
const prompt = yield* session.prompt({ sessionID: created.id, text: "Rename the file", resume: false })
|
|
yield* SessionInbox.promote(database.db, bus, created.id, "steer")
|
|
|
|
yield* Effect.gen(function* () {
|
|
const plugins = yield* PluginSupervisor.Service
|
|
yield* plugins.flush
|
|
const snapshot = yield* Snapshot.Service
|
|
const before = yield* snapshot.capture()
|
|
if (!before) throw new Error("Initial snapshot missing")
|
|
const assistantMessageID = SessionMessage.ID.create()
|
|
yield* bus.publish(SessionEvent.Step.Started, {
|
|
sessionID: created.id,
|
|
assistantMessageID,
|
|
agent: Agent.defaultID,
|
|
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.make("test-provider") },
|
|
snapshot: before,
|
|
})
|
|
yield* Effect.promise(() => fs.rename(original, renamed))
|
|
const after = yield* snapshot.capture()
|
|
if (!after) throw new Error("Renamed snapshot missing")
|
|
yield* bus.publish(SessionEvent.Step.Ended, {
|
|
sessionID: created.id,
|
|
assistantMessageID,
|
|
finish: "stop",
|
|
cost: Money.USD.zero,
|
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
snapshot: after,
|
|
files: yield* snapshot.files({ from: before, to: after }),
|
|
})
|
|
|
|
yield* Effect.promise(() => Bun.write(path.join(directory, "unrelated.txt"), "Keep this later edit.\n"))
|
|
const reverted = yield* session.revert.stage({ sessionID: created.id, messageID: prompt.id })
|
|
expect({
|
|
original: yield* Effect.promise(() => Bun.file(original).exists()),
|
|
renamed: yield* Effect.promise(() => Bun.file(renamed).exists()),
|
|
}).toEqual({ original: true, renamed: false })
|
|
expect(yield* Effect.promise(() => Bun.file(original).text())).toBe("Preserve this content.\n")
|
|
expect(reverted.files?.map((file) => [file.file, file.status])).toEqual([
|
|
["new name.txt", "deleted"],
|
|
["old name.txt", "added"],
|
|
])
|
|
expect(yield* Effect.promise(() => Bun.file(path.join(directory, "unrelated.txt")).text())).toBe(
|
|
"Keep this later edit.\n",
|
|
)
|
|
|
|
yield* session.revert.clear(created.id)
|
|
expect(yield* Effect.promise(() => Bun.file(original).exists())).toBe(false)
|
|
expect(yield* Effect.promise(() => Bun.file(renamed).text())).toBe("Preserve this content.\n")
|
|
expect(yield* Effect.promise(() => Bun.file(path.join(directory, "unrelated.txt")).text())).toBe(
|
|
"Keep this later edit.\n",
|
|
)
|
|
expect((yield* session.get(created.id)).revert).toBeUndefined()
|
|
}).pipe(Effect.provide(LocationServiceMap.Service.get(created.location)))
|
|
}),
|
|
)
|
|
})
|