mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
fix(cli): keep session reverts atomic
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect, Exit, Layer } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
@@ -242,4 +242,114 @@ describe("workspace revert status", () => {
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"keeps the conversation and workspace unchanged when a checkpoint cannot be fully restored",
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const revert = yield* SessionRevert.Service
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const session = yield* sessions.create({})
|
||||
const locked = path.join(dir, "locked")
|
||||
const protectedFile = path.join(locked, "protected.txt")
|
||||
const writableFile = path.join(dir, "writable.txt")
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
yield* Effect.promise(() => fs.mkdir(locked))
|
||||
yield* Effect.promise(() => fs.writeFile(protectedFile, "before"))
|
||||
yield* Effect.promise(() => fs.writeFile(writableFile, "before"))
|
||||
const user = yield* sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
sessionID: session.id,
|
||||
role: "user",
|
||||
agent: "default",
|
||||
model: { providerID, modelID: ModelV2.ID.make("test") },
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "change both files",
|
||||
})
|
||||
const assistant = yield* sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
sessionID: session.id,
|
||||
role: "assistant",
|
||||
parentID: user.id,
|
||||
mode: "default",
|
||||
agent: "default",
|
||||
path: { cwd: dir, root: dir },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
modelID: ModelV2.ID.make("test"),
|
||||
providerID,
|
||||
time: { created: Date.now() },
|
||||
finish: "end_turn",
|
||||
})
|
||||
const before = yield* snapshot.track()
|
||||
if (!before) throw new Error("expected snapshot")
|
||||
yield* Effect.promise(() => fs.writeFile(protectedFile, "after"))
|
||||
yield* Effect.promise(() => fs.writeFile(writableFile, "after"))
|
||||
const after = yield* snapshot.track()
|
||||
if (!after) throw new Error("expected snapshot")
|
||||
const patch = yield* snapshot.patch(before)
|
||||
yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
type: "step-start",
|
||||
snapshot: before,
|
||||
})
|
||||
yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
type: "step-finish",
|
||||
reason: "stop",
|
||||
snapshot: after,
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
})
|
||||
yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
type: "patch",
|
||||
hash: patch.hash,
|
||||
files: patch.files,
|
||||
})
|
||||
|
||||
yield* Effect.promise(() => fs.chmod(protectedFile, 0o444))
|
||||
yield* Effect.promise(() => fs.chmod(locked, 0o555))
|
||||
const outcome = yield* revert.revert({ sessionID: session.id, messageID: user.id }).pipe(
|
||||
Effect.exit,
|
||||
Effect.ensuring(
|
||||
Effect.promise(async () => {
|
||||
await fs.chmod(locked, 0o755)
|
||||
await fs.chmod(protectedFile, 0o644)
|
||||
}),
|
||||
),
|
||||
)
|
||||
const current = yield* sessions.get(session.id)
|
||||
const actual = {
|
||||
failed: Exit.isFailure(outcome),
|
||||
reverted: current.revert !== undefined,
|
||||
protected: yield* Effect.promise(() => fs.readFile(protectedFile, "utf8")),
|
||||
writable: yield* Effect.promise(() => fs.readFile(writableFile, "utf8")),
|
||||
}
|
||||
|
||||
expect(actual).toEqual({
|
||||
failed: true,
|
||||
reverted: false,
|
||||
protected: "after",
|
||||
writable: "after",
|
||||
})
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
30_000,
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user