| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- import { describe, expect, test, beforeEach, afterEach } from "bun:test"
- import path from "path"
- import { Session } from "../../src/session"
- import { SessionRevert } from "../../src/session/revert"
- import { SessionCompaction } from "../../src/session/compaction"
- import { MessageV2 } from "../../src/session/message-v2"
- import { Log } from "../../src/util/log"
- import { Instance } from "../../src/project/instance"
- import { Identifier } from "../../src/id/id"
- import { tmpdir } from "../fixture/fixture"
- const projectRoot = path.join(__dirname, "../..")
- Log.init({ print: false })
- describe("revert + compact workflow", () => {
- test("should properly handle compact command after revert", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- // Create a session
- const session = await Session.create({})
- const sessionID = session.id
- // Create a user message
- const userMsg1 = await Session.updateMessage({
- id: Identifier.ascending("message"),
- role: "user",
- sessionID,
- agent: "default",
- model: {
- providerID: "openai",
- modelID: "gpt-4",
- },
- time: {
- created: Date.now(),
- },
- })
- // Add a text part to the user message
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: userMsg1.id,
- sessionID,
- type: "text",
- text: "Hello, please help me",
- })
- // Create an assistant response message
- const assistantMsg1: MessageV2.Assistant = {
- id: Identifier.ascending("message"),
- role: "assistant",
- sessionID,
- mode: "default",
- agent: "default",
- path: {
- cwd: tmp.path,
- root: tmp.path,
- },
- cost: 0,
- tokens: {
- output: 0,
- input: 0,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- modelID: "gpt-4",
- providerID: "openai",
- parentID: userMsg1.id,
- time: {
- created: Date.now(),
- },
- finish: "end_turn",
- }
- await Session.updateMessage(assistantMsg1)
- // Add a text part to the assistant message
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg1.id,
- sessionID,
- type: "text",
- text: "Sure, I'll help you!",
- })
- // Create another user message
- const userMsg2 = await Session.updateMessage({
- id: Identifier.ascending("message"),
- role: "user",
- sessionID,
- agent: "default",
- model: {
- providerID: "openai",
- modelID: "gpt-4",
- },
- time: {
- created: Date.now(),
- },
- })
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: userMsg2.id,
- sessionID,
- type: "text",
- text: "What's the capital of France?",
- })
- // Create another assistant response
- const assistantMsg2: MessageV2.Assistant = {
- id: Identifier.ascending("message"),
- role: "assistant",
- sessionID,
- mode: "default",
- agent: "default",
- path: {
- cwd: tmp.path,
- root: tmp.path,
- },
- cost: 0,
- tokens: {
- output: 0,
- input: 0,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- modelID: "gpt-4",
- providerID: "openai",
- parentID: userMsg2.id,
- time: {
- created: Date.now(),
- },
- finish: "end_turn",
- }
- await Session.updateMessage(assistantMsg2)
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg2.id,
- sessionID,
- type: "text",
- text: "The capital of France is Paris.",
- })
- // Verify messages before revert
- let messages = await Session.messages({ sessionID })
- expect(messages.length).toBe(4) // 2 user + 2 assistant messages
- const messageIds = messages.map((m) => m.info.id)
- expect(messageIds).toContain(userMsg1.id)
- expect(messageIds).toContain(userMsg2.id)
- expect(messageIds).toContain(assistantMsg1.id)
- expect(messageIds).toContain(assistantMsg2.id)
- // Revert the last user message (userMsg2)
- await SessionRevert.revert({
- sessionID,
- messageID: userMsg2.id,
- })
- // Check that revert state is set
- let sessionInfo = await Session.get(sessionID)
- expect(sessionInfo.revert).toBeDefined()
- const revertMessageID = sessionInfo.revert?.messageID
- expect(revertMessageID).toBeDefined()
- // Messages should still be in the list (not removed yet, just marked for revert)
- messages = await Session.messages({ sessionID })
- expect(messages.length).toBe(4)
- // Now clean up the revert state (this is what the compact endpoint should do)
- await SessionRevert.cleanup(sessionInfo)
- // After cleanup, the reverted messages (those after the revert point) should be removed
- messages = await Session.messages({ sessionID })
- const remainingIds = messages.map((m) => m.info.id)
- // The revert point is somewhere in the message chain, so we should have fewer messages
- expect(messages.length).toBeLessThan(4)
- // userMsg2 and assistantMsg2 should be removed (they come after the revert point)
- expect(remainingIds).not.toContain(userMsg2.id)
- expect(remainingIds).not.toContain(assistantMsg2.id)
- // Revert state should be cleared
- sessionInfo = await Session.get(sessionID)
- expect(sessionInfo.revert).toBeUndefined()
- // Clean up
- await Session.remove(sessionID)
- },
- })
- })
- test("should properly clean up revert state before creating compaction message", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- // Create a session
- const session = await Session.create({})
- const sessionID = session.id
- // Create initial messages
- const userMsg = await Session.updateMessage({
- id: Identifier.ascending("message"),
- role: "user",
- sessionID,
- agent: "default",
- model: {
- providerID: "openai",
- modelID: "gpt-4",
- },
- time: {
- created: Date.now(),
- },
- })
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: userMsg.id,
- sessionID,
- type: "text",
- text: "Hello",
- })
- const assistantMsg: MessageV2.Assistant = {
- id: Identifier.ascending("message"),
- role: "assistant",
- sessionID,
- mode: "default",
- agent: "default",
- path: {
- cwd: tmp.path,
- root: tmp.path,
- },
- cost: 0,
- tokens: {
- output: 0,
- input: 0,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- modelID: "gpt-4",
- providerID: "openai",
- parentID: userMsg.id,
- time: {
- created: Date.now(),
- },
- finish: "end_turn",
- }
- await Session.updateMessage(assistantMsg)
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg.id,
- sessionID,
- type: "text",
- text: "Hi there!",
- })
- // Revert the user message
- await SessionRevert.revert({
- sessionID,
- messageID: userMsg.id,
- })
- // Check that revert state is set
- let sessionInfo = await Session.get(sessionID)
- expect(sessionInfo.revert).toBeDefined()
- // Simulate what the compact endpoint does: cleanup revert before creating compaction
- await SessionRevert.cleanup(sessionInfo)
- // Verify revert state is cleared
- sessionInfo = await Session.get(sessionID)
- expect(sessionInfo.revert).toBeUndefined()
- // Verify messages are properly cleaned up
- const messages = await Session.messages({ sessionID })
- expect(messages.length).toBe(0) // All messages should be reverted
- // Clean up
- await Session.remove(sessionID)
- },
- })
- })
- })
|