Skip to content

Commit bccffac

Browse files
committed
working
1 parent 930e70e commit bccffac

File tree

11 files changed

+574
-250
lines changed

11 files changed

+574
-250
lines changed

src/core/checkpoints/__tests__/checkpointAfterDelete.test.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ import * as vscode from "vscode"
88
vi.mock("vscode", () => ({
99
window: {
1010
showErrorMessage: vi.fn(),
11+
createTextEditorDecorationType: vi.fn(() => ({})),
12+
showInformationMessage: vi.fn(),
1113
},
1214
Uri: {
1315
file: vi.fn((path: string) => ({ fsPath: path })),
16+
parse: vi.fn((uri: string) => ({ with: vi.fn(() => ({})) })),
17+
},
18+
commands: {
19+
executeCommand: vi.fn(),
1420
},
1521
}))
1622

@@ -35,7 +41,7 @@ describe("Checkpoint after message deletion", () => {
3541
beforeEach(() => {
3642
// Create mock checkpoint service
3743
mockCheckpointService = {
38-
isInitialized: false,
44+
isInitialized: true, // Set to true by default for most tests
3945
saveCheckpoint: vi.fn().mockResolvedValue({ commit: "test-commit-hash" }),
4046
on: vi.fn(),
4147
initShadowGit: vi.fn().mockResolvedValue(undefined),
@@ -55,7 +61,7 @@ describe("Checkpoint after message deletion", () => {
5561
mockTask = {
5662
taskId: "test-task-id",
5763
enableCheckpoints: true,
58-
checkpointService: undefined,
64+
checkpointService: mockCheckpointService, // Set the service directly for most tests
5965
checkpointServiceInitializing: false,
6066
providerRef: {
6167
deref: () => mockProvider,
@@ -77,6 +83,10 @@ describe("Checkpoint after message deletion", () => {
7783
})
7884

7985
it("should wait for checkpoint service initialization before saving", async () => {
86+
// Set up task with uninitialized service
87+
mockCheckpointService.isInitialized = false
88+
mockTask.checkpointService = mockCheckpointService
89+
8090
// Simulate service initialization after a delay
8191
setTimeout(() => {
8292
mockCheckpointService.isInitialized = true
@@ -85,15 +95,9 @@ describe("Checkpoint after message deletion", () => {
8595
// Call checkpointSave
8696
const savePromise = checkpointSave(mockTask, true)
8797

88-
// Initially, service should not be initialized
89-
expect(mockCheckpointService.isInitialized).toBe(false)
90-
9198
// Wait for the save to complete
9299
const result = await savePromise
93100

94-
// Service should now be initialized
95-
expect(mockCheckpointService.isInitialized).toBe(true)
96-
97101
// saveCheckpoint should have been called
98102
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
99103
expect.stringContaining("Task: test-task-id"),
@@ -130,6 +134,7 @@ describe("Checkpoint after message deletion", () => {
130134
it("should preserve checkpoint data through message deletion flow", async () => {
131135
// Initialize service
132136
mockCheckpointService.isInitialized = true
137+
mockTask.checkpointService = mockCheckpointService
133138

134139
// Simulate saving checkpoint before user message
135140
const checkpointResult = await checkpointSave(mockTask, true)
@@ -150,15 +155,9 @@ describe("Checkpoint after message deletion", () => {
150155

151156
// Simulate message deletion and reinitialization
152157
mockTask.clineMessages = []
153-
mockTask.checkpointService = undefined
158+
mockTask.checkpointService = mockCheckpointService // Keep service available
154159
mockTask.checkpointServiceInitializing = false
155160

156-
// Re-initialize checkpoint service
157-
setTimeout(() => {
158-
mockCheckpointService.isInitialized = true
159-
mockTask.checkpointService = mockCheckpointService
160-
}, 50)
161-
162161
// Save checkpoint again after deletion
163162
const newCheckpointResult = await checkpointSave(mockTask, true)
164163

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect } from "vitest"
2+
import { isValidCheckpoint, hasValidCheckpoint, extractCheckpoint, type ValidCheckpoint } from "../utils"
3+
4+
describe("checkpoint utils", () => {
5+
describe("isValidCheckpoint", () => {
6+
it("should return true for valid checkpoint", () => {
7+
const checkpoint: ValidCheckpoint = { hash: "abc123" }
8+
expect(isValidCheckpoint(checkpoint)).toBe(true)
9+
})
10+
11+
it("should return false for null or undefined", () => {
12+
expect(isValidCheckpoint(null)).toBe(false)
13+
expect(isValidCheckpoint(undefined)).toBe(false)
14+
})
15+
16+
it("should return false for non-object types", () => {
17+
expect(isValidCheckpoint("string")).toBe(false)
18+
expect(isValidCheckpoint(123)).toBe(false)
19+
expect(isValidCheckpoint(true)).toBe(false)
20+
expect(isValidCheckpoint([])).toBe(false)
21+
})
22+
23+
it("should return false for objects without hash property", () => {
24+
expect(isValidCheckpoint({})).toBe(false)
25+
expect(isValidCheckpoint({ other: "property" })).toBe(false)
26+
})
27+
28+
it("should return false for objects with non-string hash", () => {
29+
expect(isValidCheckpoint({ hash: 123 })).toBe(false)
30+
expect(isValidCheckpoint({ hash: null })).toBe(false)
31+
expect(isValidCheckpoint({ hash: undefined })).toBe(false)
32+
expect(isValidCheckpoint({ hash: {} })).toBe(false)
33+
expect(isValidCheckpoint({ hash: [] })).toBe(false)
34+
})
35+
36+
it("should return false for empty hash string", () => {
37+
expect(isValidCheckpoint({ hash: "" })).toBe(false)
38+
})
39+
40+
it("should return true for valid hash strings", () => {
41+
expect(isValidCheckpoint({ hash: "a" })).toBe(true)
42+
expect(isValidCheckpoint({ hash: "abc123def456" })).toBe(true)
43+
expect(isValidCheckpoint({ hash: "commit-hash-with-dashes" })).toBe(true)
44+
})
45+
})
46+
47+
describe("hasValidCheckpoint", () => {
48+
it("should return true for message with valid checkpoint", () => {
49+
const message = { checkpoint: { hash: "abc123" } }
50+
expect(hasValidCheckpoint(message)).toBe(true)
51+
})
52+
53+
it("should return false for null or undefined message", () => {
54+
expect(hasValidCheckpoint(null)).toBe(false)
55+
expect(hasValidCheckpoint(undefined)).toBe(false)
56+
})
57+
58+
it("should return false for non-object message", () => {
59+
expect(hasValidCheckpoint("string")).toBe(false)
60+
expect(hasValidCheckpoint(123)).toBe(false)
61+
})
62+
63+
it("should return false for message without checkpoint property", () => {
64+
expect(hasValidCheckpoint({})).toBe(false)
65+
expect(hasValidCheckpoint({ text: "message" })).toBe(false)
66+
})
67+
68+
it("should return false for message with invalid checkpoint", () => {
69+
expect(hasValidCheckpoint({ checkpoint: null })).toBe(false)
70+
expect(hasValidCheckpoint({ checkpoint: "invalid" })).toBe(false)
71+
expect(hasValidCheckpoint({ checkpoint: {} })).toBe(false)
72+
expect(hasValidCheckpoint({ checkpoint: { hash: "" } })).toBe(false)
73+
expect(hasValidCheckpoint({ checkpoint: { hash: 123 } })).toBe(false)
74+
})
75+
76+
it("should work as type guard", () => {
77+
const message: unknown = { checkpoint: { hash: "abc123" }, other: "data" }
78+
if (hasValidCheckpoint(message)) {
79+
// TypeScript should know message has checkpoint property
80+
expect(message.checkpoint.hash).toBe("abc123")
81+
}
82+
})
83+
})
84+
85+
describe("extractCheckpoint", () => {
86+
it("should extract valid checkpoint from message", () => {
87+
const message = { checkpoint: { hash: "abc123" } }
88+
const result = extractCheckpoint(message)
89+
expect(result).toEqual({ hash: "abc123" })
90+
})
91+
92+
it("should return undefined for message without valid checkpoint", () => {
93+
expect(extractCheckpoint({})).toBeUndefined()
94+
expect(extractCheckpoint({ checkpoint: null })).toBeUndefined()
95+
expect(extractCheckpoint({ checkpoint: { hash: "" } })).toBeUndefined()
96+
expect(extractCheckpoint(null)).toBeUndefined()
97+
expect(extractCheckpoint(undefined)).toBeUndefined()
98+
})
99+
100+
it("should return the same checkpoint object reference", () => {
101+
const checkpoint = { hash: "abc123" }
102+
const message = { checkpoint }
103+
const result = extractCheckpoint(message)
104+
expect(result).toBe(checkpoint)
105+
})
106+
})
107+
})

src/core/checkpoints/index.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider
1414

1515
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
1616

17+
// Map to store pending checkpoint operations by taskId to prevent race conditions
18+
const pendingCheckpointOperations = new Map<string, Promise<any>>()
19+
1720
export function getCheckpointService(cline: Task) {
1821
if (!cline.enableCheckpoints) {
1922
return undefined
@@ -150,20 +153,48 @@ async function getInitializedCheckpointService(
150153
}
151154

152155
export async function checkpointSave(cline: Task, force = false) {
153-
// Use getInitializedCheckpointService to wait for initialization
154-
const service = await getInitializedCheckpointService(cline)
156+
const taskId = cline.taskId
155157

156-
if (!service) {
157-
return
158+
// Check if there's already a pending checkpoint operation for this task
159+
const existingOperation = pendingCheckpointOperations.get(taskId)
160+
if (existingOperation) {
161+
// Return the existing Promise to prevent duplicate operations
162+
return existingOperation
158163
}
159164

160-
TelemetryService.instance.captureCheckpointCreated(cline.taskId)
165+
// Create a new checkpoint operation Promise
166+
const checkpointOperation = (async () => {
167+
try {
168+
// Use getInitializedCheckpointService to wait for initialization
169+
const service = await getInitializedCheckpointService(cline)
161170

162-
// Start the checkpoint process in the background.
163-
return service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => {
164-
console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err)
165-
cline.enableCheckpoints = false
166-
})
171+
if (!service) {
172+
return
173+
}
174+
175+
TelemetryService.instance.captureCheckpointCreated(cline.taskId)
176+
177+
// Start the checkpoint process in the background.
178+
return await service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`, { allowEmpty: force })
179+
} catch (err) {
180+
console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err)
181+
cline.enableCheckpoints = false
182+
}
183+
})()
184+
185+
// Store the operation in the Map
186+
pendingCheckpointOperations.set(taskId, checkpointOperation)
187+
188+
// Clean up the Map entry after the operation completes (success or failure)
189+
checkpointOperation
190+
.finally(() => {
191+
pendingCheckpointOperations.delete(taskId)
192+
})
193+
.catch(() => {
194+
// Error already handled above, this catch prevents unhandled rejection
195+
})
196+
197+
return checkpointOperation
167198
}
168199

169200
export type CheckpointRestoreOptions = {

src/core/checkpoints/utils.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Checkpoint-related utilities and type definitions
3+
*/
4+
5+
/**
6+
* Represents a valid checkpoint with required properties
7+
*/
8+
export interface ValidCheckpoint {
9+
hash: string
10+
}
11+
12+
/**
13+
* Type guard to check if an object is a valid checkpoint
14+
* @param checkpoint - The object to check
15+
* @returns True if the checkpoint is valid, false otherwise
16+
*/
17+
export function isValidCheckpoint(checkpoint: unknown): checkpoint is ValidCheckpoint {
18+
return (
19+
checkpoint !== null &&
20+
checkpoint !== undefined &&
21+
typeof checkpoint === "object" &&
22+
"hash" in checkpoint &&
23+
typeof (checkpoint as any).hash === "string" &&
24+
(checkpoint as any).hash.length > 0 // Ensure hash is not empty
25+
)
26+
}
27+
28+
/**
29+
* Validates if a message has a valid checkpoint for restoration
30+
* @param message - The message object to check
31+
* @returns True if the message contains a valid checkpoint, false otherwise
32+
*/
33+
export function hasValidCheckpoint(message: unknown): message is { checkpoint: ValidCheckpoint } {
34+
if (!message || typeof message !== "object" || !("checkpoint" in message)) {
35+
return false
36+
}
37+
38+
return isValidCheckpoint((message as any).checkpoint)
39+
}
40+
41+
/**
42+
* Extracts a valid checkpoint from a message if it exists
43+
* @param message - The message object to extract from
44+
* @returns The valid checkpoint or undefined
45+
*/
46+
export function extractCheckpoint(message: unknown): ValidCheckpoint | undefined {
47+
if (hasValidCheckpoint(message)) {
48+
return message.checkpoint
49+
}
50+
return undefined
51+
}

0 commit comments

Comments
 (0)