Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const experimentIds = [
"preventFocusDisruption",
"imageGeneration",
"runSlashCommand",
"filesChangedOverview",
] as const

export const experimentIdsSchema = z.enum(experimentIds)
Expand All @@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
preventFocusDisruption: z.boolean().optional(),
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
filesChangedOverview: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
21 changes: 21 additions & 0 deletions packages/types/src/file-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type FileChangeType = "create" | "delete" | "edit"

export interface FileChange {
uri: string
type: FileChangeType
// Note: Checkpoint hashes are for backend use, but can be included
fromCheckpoint: string
toCheckpoint: string
// Line count information for display
linesAdded?: number
linesRemoved?: number
}

/**
* Represents the set of file changes for the webview.
* The `files` property is an array for easy serialization.
*/
export interface FileChangeset {
baseCheckpoint: string
files: FileChange[]
}
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from "./type-fu.js"
export * from "./vscode.js"

export * from "./providers/index.js"
export * from "./file-changes.js"
1 change: 1 addition & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { Task } from "../task/Task"
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
// Live FCO updates removed: FCO now updates on checkpoint events only

/**
* Processes and presents assistant message content to the user interface.
Expand Down
5 changes: 3 additions & 2 deletions src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe("Checkpoint functionality", () => {
getDiff: vi.fn().mockResolvedValue([]),
on: vi.fn(),
initShadowGit: vi.fn().mockResolvedValue(undefined),
getCurrentCheckpoint: vi.fn().mockReturnValue("base-hash"),
}

// Create mock provider
Expand Down Expand Up @@ -111,7 +112,7 @@ describe("Checkpoint functionality", () => {
// saveCheckpoint should have been called
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
expect.stringContaining("Task: test-task-id"),
{ allowEmpty: true, suppressMessage: false },
{ allowEmpty: true, files: undefined },
)

// Result should contain the commit hash
Expand Down Expand Up @@ -329,7 +330,7 @@ describe("Checkpoint functionality", () => {
})
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
"vscode.changes",
"Changes compare with next checkpoint",
"Changes since previous checkpoint",
expect.any(Array),
)
})
Expand Down
53 changes: 53 additions & 0 deletions src/core/checkpoints/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { vitest } from "vitest"

export const createMockTask = (options: {
taskId: string
hasExistingCheckpoints?: boolean
enableCheckpoints?: boolean
provider?: any
}) => {
const mockTask = {
taskId: options.taskId,
instanceId: "test-instance",
rootTask: undefined as any,
parentTask: undefined as any,
taskNumber: 1,
workspacePath: "/mock/workspace",
enableCheckpoints: options.enableCheckpoints ?? true,
checkpointService: null as any,
checkpointServiceInitializing: false,
ongoingCheckpointSaves: new Map(),
clineMessages: options.hasExistingCheckpoints
? [{ say: "checkpoint_saved", ts: Date.now(), text: "existing-checkpoint-hash" }]
: [],
providerRef: {
deref: () => options.provider || createMockProvider(),
},
fileContextTracker: {},
todoList: undefined,
}

return mockTask
}

export const createMockProvider = () => ({
getFileChangeManager: vitest.fn(),
ensureFileChangeManager: vitest.fn(),
log: vitest.fn(),
postMessageToWebview: vitest.fn(),
getGlobalState: vitest.fn(),
})

// Mock checkpoint service for testing
export const createMockCheckpointService = () => ({
saveCheckpoint: vitest.fn().mockResolvedValue({
commit: "mock-checkpoint-hash",
message: "Mock checkpoint",
}),
restoreCheckpoint: vitest.fn().mockResolvedValue(true),
getDiff: vitest.fn().mockResolvedValue([]),
getCheckpoints: vitest.fn().mockReturnValue([]),
getCurrentCheckpoint: vitest.fn().mockReturnValue("mock-current-checkpoint"),
initShadowGit: vitest.fn().mockResolvedValue(true),
baseHash: "mock-base-hash",
})
227 changes: 227 additions & 0 deletions src/core/checkpoints/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Use doMock to apply the mock dynamically
vitest.doMock("../../utils/path", () => ({
getWorkspacePath: vitest.fn(() => {
console.log("getWorkspacePath mock called, returning:", "/mock/workspace")
return "/mock/workspace"
}),
}))

// Mock the RepoPerTaskCheckpointService
vitest.mock("../../../services/checkpoints", () => ({
RepoPerTaskCheckpointService: {
create: vitest.fn(),
},
}))

// Mock the TelemetryService to prevent unhandled rejections
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureCheckpointCreated: vitest.fn(),
captureCheckpointRestored: vitest.fn(),
captureCheckpointDiffed: vitest.fn(),
},
},
}))

import { describe, it, expect, beforeEach, afterEach, vitest } from "vitest"
import * as path from "path"
import * as fs from "fs/promises"
import * as os from "os"
import { EventEmitter } from "events"

// Import these modules after mocks are set up
let getCheckpointService: any
let RepoPerTaskCheckpointService: any

// Set up the imports after mocks
beforeAll(async () => {
const checkpointsModule = await import("../index")
const checkpointServiceModule = await import("../../../services/checkpoints")
getCheckpointService = checkpointsModule.getCheckpointService
RepoPerTaskCheckpointService = checkpointServiceModule.RepoPerTaskCheckpointService
})

// Mock the FileChangeManager to avoid complex dependencies
const mockFileChangeManager = {
_baseline: "HEAD" as string,
getChanges: vitest.fn(),
updateBaseline: vitest.fn(),
setFiles: vitest.fn(),
getLLMOnlyChanges: vitest.fn(),
}

// Create a temporary directory for mock global storage
let mockGlobalStorageDir: string

// Mock the provider
const mockProvider = {
getFileChangeManager: vitest.fn(() => mockFileChangeManager),
log: vitest.fn(),
get context() {
return {
globalStorageUri: {
fsPath: mockGlobalStorageDir,
},
}
},
}

// Mock the Task object with proper typing
const createMockTask = (options: { taskId: string; hasExistingCheckpoints: boolean; enableCheckpoints?: boolean }) => {
const mockTask = {
taskId: options.taskId,
instanceId: "test-instance",
rootTask: undefined as any,
parentTask: undefined as any,
taskNumber: 1,
workspacePath: "/mock/workspace",
enableCheckpoints: options.enableCheckpoints ?? true,
checkpointService: null as any,
checkpointServiceInitializing: false,
ongoingCheckpointSaves: new Map(),
clineMessages: options.hasExistingCheckpoints
? [{ say: "checkpoint_saved", ts: Date.now(), text: "existing-checkpoint-hash" }]
: [],
providerRef: {
deref: () => mockProvider,
},
fileContextTracker: {},
// Add minimal required properties to satisfy Task interface
todoList: undefined,
userMessageContent: "",
apiConversationHistory: [],
customInstructions: "",
alwaysAllowReadOnly: false,
alwaysAllowWrite: false,
alwaysAllowExecute: false,
alwaysAllowBrowser: false,
alwaysAllowMcp: false,
createdAt: Date.now(),
historyErrors: [],
askResponse: undefined,
askResponseText: "",
abort: vitest.fn(),
isAborting: false,
} as any // Cast to any to avoid needing to implement all Task methods
return mockTask
}

describe("getCheckpointService orchestration", () => {
let tmpDir: string
let mockService: any

beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "checkpoint-test-"))
mockGlobalStorageDir = path.join(tmpDir, "global-storage")
await fs.mkdir(mockGlobalStorageDir, { recursive: true })

// Reset mocks
vitest.clearAllMocks()

// Override the global vscode mock to have a workspace folder
const vscode = await import("vscode")
// @ts-ignore - Mock the workspace.workspaceFolders
vscode.workspace.workspaceFolders = [
{
uri: {
fsPath: "/mock/workspace",
},
},
]

// Mock the checkpoint service
mockService = new EventEmitter()
mockService.baseHash = "mock-base-hash-abc123"
mockService.getCurrentCheckpoint = vitest.fn(() => "mock-current-checkpoint-def456")
mockService.isInitialized = true
mockService.initShadowGit = vitest.fn(() => {
// Simulate the initialize event being emitted after initShadowGit completes
setImmediate(() => {
mockService.emit("initialize")
})
return Promise.resolve()
})
mockService.saveCheckpoint = vitest.fn(() => {
return Promise.resolve({
commit: "mock-checkpoint-hash",
message: "Mock checkpoint",
})
})

// Mock the service creation
;(RepoPerTaskCheckpointService.create as any).mockReturnValue(mockService)
})

afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true })
vitest.restoreAllMocks()
})

describe("Service creation and caching", () => {
it("should create and return a new checkpoint service", async () => {
const task = createMockTask({
taskId: "new-task-123",
hasExistingCheckpoints: false,
})

const service = await getCheckpointService(task)
console.log("Service returned:", service)
expect(service).toBe(mockService)
expect(RepoPerTaskCheckpointService.create).toHaveBeenCalledWith({
taskId: "new-task-123",
shadowDir: mockGlobalStorageDir,
workspaceDir: "/mock/workspace",
log: expect.any(Function),
})
})

it("should return existing service if already initialized", async () => {
const task = createMockTask({
taskId: "existing-service-task",
hasExistingCheckpoints: false,
})

// Set existing checkpoint service
task.checkpointService = mockService

const service = await getCheckpointService(task)
expect(service).toBe(mockService)

// Should not create a new service
expect(RepoPerTaskCheckpointService.create).not.toHaveBeenCalled()
})

it("should return undefined when checkpoints are disabled", async () => {
const task = createMockTask({
taskId: "disabled-task",
hasExistingCheckpoints: false,
enableCheckpoints: false,
})

const service = await getCheckpointService(task)
expect(service).toBeUndefined()
})
})

describe("Service initialization", () => {
it("should call initShadowGit and set up event handlers", async () => {
const task = createMockTask({
taskId: "init-test-task",
hasExistingCheckpoints: false,
})

const service = await getCheckpointService(task)
expect(service).toBe(mockService)

// initShadowGit should be called
expect(mockService.initShadowGit).toHaveBeenCalled()

// Wait for the initialize event to be emitted and the service to be assigned
await new Promise((resolve) => setImmediate(resolve))

// Service should be assigned to task after initialization
expect(task.checkpointService).toBe(mockService)
})
})
})
Loading
Loading