mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-30 03:02:28 +00:00
Separate ID-bound Session policy from host routing. Bind Inbox and Location preparation dependencies at construction, preserve admission and execution semantics, and cover the extracted ownership contracts directly.
122 lines
5.9 KiB
TypeScript
122 lines
5.9 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 { SessionRevert } from "@opencode-ai/core/session/revert"
|
|
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 { tmpdirScoped } 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* tmpdirScoped()
|
|
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")
|
|
const services = LocationServiceMap.Service.get(created.location)
|
|
const revert = yield* SessionRevert.Service.pipe(Effect.provide(services))
|
|
expect(yield* SessionRevert.Service.pipe(Effect.provide(services))).toBe(revert)
|
|
|
|
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)))
|
|
}),
|
|
// Real Location/plugin startup and Git snapshots can exceed five seconds under CI load.
|
|
{ timeout: 15_000 },
|
|
)
|
|
})
|