Browse Source

less duplication

Mark IJbema 4 tháng trước cách đây
mục cha
commit
6563f031d2

+ 16 - 150
src/test-llm-autocompletion/auto-trigger-strategy.ts

@@ -1,142 +1,11 @@
 import { LLMClient } from "./llm-client.js"
 import { AutoTriggerStrategy } from "../services/ghost/strategies/AutoTriggerStrategy.js"
-import { GhostSuggestionContext } from "./types.js"
+import { GhostSuggestionContext } from "../services/ghost/types.js"
+import { MockTextDocument } from "../services/mocking/MockTextDocument.js"
+import { CURSOR_MARKER } from "../services/ghost/ghostConstants.js"
+import { GhostStreamingParser } from "../services/ghost/GhostStreamingParser.js"
 import * as vscode from "./mock-vscode.js"
 
-const CURSOR_MARKER = "<<<AUTOCOMPLETE_HERE>>>"
-
-/**
- * Mock TextDocument implementation for testing
- */
-class MockTextDocument implements vscode.TextDocument {
-	private _text: string
-	private _languageId: string
-
-	constructor(text: string, languageId: string = "javascript") {
-		this._text = text
-		this._languageId = languageId
-	}
-
-	get uri(): vscode.Uri {
-		return vscode.Uri.parse("file:///test.js")
-	}
-
-	get fileName(): string {
-		return "test.js"
-	}
-
-	get isUntitled(): boolean {
-		return false
-	}
-
-	get languageId(): string {
-		return this._languageId
-	}
-
-	get version(): number {
-		return 1
-	}
-
-	get isDirty(): boolean {
-		return false
-	}
-
-	get isClosed(): boolean {
-		return false
-	}
-
-	get eol(): vscode.EndOfLine {
-		return vscode.EndOfLine.LF
-	}
-
-	get lineCount(): number {
-		return this._text.split("\n").length
-	}
-
-	get encoding(): string {
-		return "utf8"
-	}
-
-	save(): Promise<boolean> {
-		return Promise.resolve(true)
-	}
-
-	lineAt(lineOrPosition: number | vscode.Position): vscode.TextLine {
-		const lineNumber = typeof lineOrPosition === "number" ? lineOrPosition : lineOrPosition.line
-		const lines = this._text.split("\n")
-		const text = lines[lineNumber] || ""
-
-		return {
-			lineNumber,
-			text,
-			range: new vscode.Range(lineNumber, 0, lineNumber, text.length),
-			rangeIncludingLineBreak: new vscode.Range(lineNumber, 0, lineNumber + 1, 0),
-			firstNonWhitespaceCharacterIndex: text.search(/\S/),
-			isEmptyOrWhitespace: text.trim().length === 0,
-		}
-	}
-
-	offsetAt(position: vscode.Position): number {
-		const lines = this._text.split("\n")
-		let offset = 0
-
-		for (let i = 0; i < position.line; i++) {
-			offset += lines[i].length + 1 // +1 for newline
-		}
-
-		offset += position.character
-		return offset
-	}
-
-	positionAt(offset: number): vscode.Position {
-		const lines = this._text.split("\n")
-		let currentOffset = 0
-
-		for (let i = 0; i < lines.length; i++) {
-			const lineLength = lines[i].length + 1
-			if (currentOffset + lineLength > offset) {
-				return new vscode.Position(i, offset - currentOffset)
-			}
-			currentOffset += lineLength
-		}
-
-		return new vscode.Position(lines.length - 1, lines[lines.length - 1].length)
-	}
-
-	getText(range?: vscode.Range): string {
-		if (!range) return this._text
-
-		const startOffset = this.offsetAt(range.start)
-		const endOffset = this.offsetAt(range.end)
-		return this._text.substring(startOffset, endOffset)
-	}
-
-	getWordRangeAtPosition(position: vscode.Position, regex?: RegExp): vscode.Range | undefined {
-		const line = this.lineAt(position).text
-		const wordPattern = regex || /\b\w+\b/g
-
-		let match
-		while ((match = wordPattern.exec(line)) !== null) {
-			const start = match.index
-			const end = start + match[0].length
-
-			if (position.character >= start && position.character <= end) {
-				return new vscode.Range(position.line, start, position.line, end)
-			}
-		}
-
-		return undefined
-	}
-
-	validateRange(range: vscode.Range): vscode.Range {
-		return range
-	}
-
-	validatePosition(position: vscode.Position): vscode.Position {
-		return position
-	}
-}
-
 export class AutoTriggerStrategyTester {
 	private llmClient: LLMClient
 	private strategy: AutoTriggerStrategy
@@ -150,19 +19,19 @@ export class AutoTriggerStrategyTester {
 	 * Converts test input to GhostSuggestionContext
 	 */
 	private createContext(code: string, cursorPosition: { line: number; character: number }): GhostSuggestionContext {
-		// Insert cursor marker into the code at the specified position
 		const lines = code.split("\n")
 		const line = lines[cursorPosition.line]
 		lines[cursorPosition.line] =
 			line.slice(0, cursorPosition.character) + CURSOR_MARKER + line.slice(cursorPosition.character)
 		const codeWithMarker = lines.join("\n")
 
-		const document = new MockTextDocument(codeWithMarker)
+		const uri = vscode.Uri.parse("file:///test.js")
+		const document = new MockTextDocument(uri, codeWithMarker)
 		const position = new vscode.Position(cursorPosition.line, cursorPosition.character)
 		const range = new vscode.Range(position, position)
 
 		return {
-			document,
+			document: document as any,
 			range,
 			recentOperations: [],
 			diagnostics: [],
@@ -191,20 +60,17 @@ export class AutoTriggerStrategyTester {
 	}
 
 	parseCompletion(xmlResponse: string): { search: string; replace: string }[] {
-		const changes: { search: string; replace: string }[] = []
-
-		// Parse XML response to extract change blocks
-		const changeRegex =
-			/<change>\s*<search><!\[CDATA\[(.*?)\]\]><\/search>\s*<replace><!\[CDATA\[(.*?)\]\]><\/replace>\s*<\/change>/gs
+		const parser = new GhostStreamingParser()
 
-		let match
-		while ((match = changeRegex.exec(xmlResponse)) !== null) {
-			changes.push({
-				search: match[1],
-				replace: match[2],
-			})
+		const dummyContext: GhostSuggestionContext = {
+			document: new MockTextDocument(vscode.Uri.parse("file:///test.js"), "") as any,
+			range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)),
 		}
 
-		return changes
+		parser.initialize(dummyContext)
+		parser.processChunk(xmlResponse)
+		parser.finishStream()
+
+		return parser.getCompletedChanges()
 	}
 }

+ 2 - 9
src/test-llm-autocompletion/llm-client.ts

@@ -1,11 +1,9 @@
 import OpenAI from "openai"
 import { config } from "dotenv"
+import { DEFAULT_HEADERS } from "../api/providers/constants.js"
 
 config()
 
-// Import the version from package.json
-const packageVersion = "4.97.1" // From src/package.json
-
 export interface LLMResponse {
 	content: string
 	provider: string
@@ -45,18 +43,13 @@ export class LLMClient {
 			throw new Error("KILOCODE_API_KEY is required for Kilocode provider")
 		}
 
-		// Extract base URL from token (same logic as kilocode-openrouter.ts)
 		const baseUrl = getKiloBaseUriFromToken(process.env.KILOCODE_API_KEY)
 
-		// Use the same headers as the main Kilocode extension
 		this.openai = new OpenAI({
 			baseURL: `${baseUrl}/api/openrouter/`,
 			apiKey: process.env.KILOCODE_API_KEY,
 			defaultHeaders: {
-				"HTTP-Referer": "https://kilocode.ai",
-				"X-Title": "Kilo Code",
-				"X-KILOCODE-VERSION": packageVersion,
-				"User-Agent": `Kilo-Code/${packageVersion}`,
+				...DEFAULT_HEADERS,
 				"X-KILOCODE-TESTER": "SUPPRESS",
 			},
 		})

+ 35 - 0
src/test-llm-autocompletion/mock-vscode.ts

@@ -1,10 +1,34 @@
 // Mock vscode types for testing outside of VSCode environment
 
+export type Thenable<T> = Promise<T>
+
 export enum EndOfLine {
 	LF = 1,
 	CRLF = 2,
 }
 
+export enum DiagnosticSeverity {
+	Error = 0,
+	Warning = 1,
+	Information = 2,
+	Hint = 3,
+}
+
+export interface Diagnostic {
+	range: Range
+	message: string
+	severity: DiagnosticSeverity
+	source?: string
+	code?: string | number
+}
+
+export const workspace = {
+	asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string {
+		const path = typeof pathOrUri === "string" ? pathOrUri : pathOrUri.path
+		return path.split("/").pop() || path
+	},
+}
+
 export class Position {
 	readonly line: number
 	readonly character: number
@@ -203,6 +227,7 @@ export interface TextDocument {
 	readonly eol: EndOfLine
 	readonly lineCount: number
 	readonly encoding?: string
+	readonly notebook?: any
 	save(): Thenable<boolean>
 	lineAt(line: number): TextLine
 	lineAt(position: Position): TextLine
@@ -214,6 +239,16 @@ export interface TextDocument {
 	validatePosition(position: Position): Position
 }
 
+export interface TextEditor {
+	readonly document: TextDocument
+	readonly selection: Selection
+	readonly selections: readonly Selection[]
+	readonly visibleRanges: readonly Range[]
+	readonly options: any
+	readonly viewColumn?: any
+	edit(callback: (editBuilder: any) => void): Thenable<boolean>
+}
+
 export class Selection extends Range {
 	readonly anchor: Position
 	readonly active: Position

+ 3 - 1
src/test-llm-autocompletion/package.json

@@ -10,7 +10,9 @@
 	"dependencies": {
 		"@anthropic-ai/sdk": "^0.51.0",
 		"openai": "^5.12.2",
-		"dotenv": "^16.4.5"
+		"dotenv": "^16.4.5",
+		"diff": "^5.1.0",
+		"web-tree-sitter": "^0.20.8"
 	},
 	"devDependencies": {
 		"@types/node": "^20.0.0",

+ 26 - 24
src/test-llm-autocompletion/test-cases.ts

@@ -1,3 +1,5 @@
+import { CURSOR_MARKER } from "../services/ghost/ghostConstants.js"
+
 export interface TestCase {
 	name: string
 	category: string
@@ -13,7 +15,7 @@ export const testCases: TestCase[] = [
 		name: "closing-brace",
 		category: "Basic Syntax",
 		input: `function test() {
-  console.log('hello')<<<AUTOCOMPLETE_HERE>>>`,
+		console.log('hello')${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 23 },
 		expectedPatterns: ["}", "\\n}"],
 		description: "Should complete closing brace for function",
@@ -21,7 +23,7 @@ export const testCases: TestCase[] = [
 	{
 		name: "semicolon",
 		category: "Basic Syntax",
-		input: `const x = 42<<<AUTOCOMPLETE_HERE>>>`,
+		input: `const x = 42${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 12 },
 		expectedPatterns: [";"],
 		description: "Should add semicolon after variable declaration",
@@ -29,7 +31,7 @@ export const testCases: TestCase[] = [
 	{
 		name: "closing-bracket",
 		category: "Basic Syntax",
-		input: `const arr = [1, 2, 3<<<AUTOCOMPLETE_HERE>>>`,
+		input: `const arr = [1, 2, 3${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 20 },
 		expectedPatterns: ["]"],
 		description: "Should complete closing bracket for array",
@@ -37,7 +39,7 @@ export const testCases: TestCase[] = [
 	{
 		name: "closing-parenthesis",
 		category: "Basic Syntax",
-		input: `console.log('test'<<<AUTOCOMPLETE_HERE>>>`,
+		input: `console.log('test'${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 18 },
 		expectedPatterns: ["\\)"],
 		description: "Should complete closing parenthesis for function call",
@@ -48,7 +50,7 @@ export const testCases: TestCase[] = [
 		name: "property-access",
 		category: "Property Access",
 		input: `const obj = { name: 'test', value: 42 };
-obj.<<<AUTOCOMPLETE_HERE>>>`,
+obj.${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 4 },
 		expectedPatterns: ["name", "value"],
 		description: "Should suggest object properties",
@@ -57,7 +59,7 @@ obj.<<<AUTOCOMPLETE_HERE>>>`,
 		name: "array-method",
 		category: "Method Access",
 		input: `const arr = [1, 2, 3];
-arr.<<<AUTOCOMPLETE_HERE>>>`,
+arr.${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 4 },
 		expectedPatterns: ["map", "filter", "forEach", "push", "pop", "length"],
 		description: "Should suggest array methods",
@@ -66,7 +68,7 @@ arr.<<<AUTOCOMPLETE_HERE>>>`,
 		name: "string-method",
 		category: "Method Access",
 		input: `const str = 'hello world';
-str.<<<AUTOCOMPLETE_HERE>>>`,
+str.${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 4 },
 		expectedPatterns: ["substring", "charAt", "indexOf", "length", "toUpperCase", "toLowerCase"],
 		description: "Should suggest string methods",
@@ -76,7 +78,7 @@ str.<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "function-body",
 		category: "Function Declaration",
-		input: `function calculateSum(a, b) <<<AUTOCOMPLETE_HERE>>>`,
+		input: `function calculateSum(a, b) ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 29 },
 		expectedPatterns: ["{", "{\\n", "{\\s*\\n\\s*return a \\+ b"],
 		description: "Should complete function body opening",
@@ -84,7 +86,7 @@ str.<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "arrow-function",
 		category: "Function Declaration",
-		input: `const add = (a, b) <<<AUTOCOMPLETE_HERE>>>`,
+		input: `const add = (a, b) ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 20 },
 		expectedPatterns: ["=>", "=> {", "=> a \\+ b"],
 		description: "Should complete arrow function syntax",
@@ -93,7 +95,7 @@ str.<<<AUTOCOMPLETE_HERE>>>`,
 		name: "function-call-args",
 		category: "Function Call",
 		input: `function greet(name) { return 'Hello ' + name; }
-greet(<<<AUTOCOMPLETE_HERE>>>`,
+greet(${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 6 },
 		expectedPatterns: ["\\)", "'", '"'],
 		description: "Should suggest function call argument completion",
@@ -103,7 +105,7 @@ greet(<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "variable-assignment",
 		category: "Variable Assignment",
-		input: `const userName = <<<AUTOCOMPLETE_HERE>>>`,
+		input: `const userName = ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 17 },
 		expectedPatterns: ["'", '"', "null", "undefined"],
 		description: "Should suggest common variable assignments",
@@ -111,7 +113,7 @@ greet(<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "array-declaration",
 		category: "Variable Assignment",
-		input: `const numbers = <<<AUTOCOMPLETE_HERE>>>`,
+		input: `const numbers = ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 16 },
 		expectedPatterns: ["\\[", "\\[]"],
 		description: "Should suggest array initialization",
@@ -119,7 +121,7 @@ greet(<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "object-declaration",
 		category: "Variable Assignment",
-		input: `const config = <<<AUTOCOMPLETE_HERE>>>`,
+		input: `const config = ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 15 },
 		expectedPatterns: ["{", "{}", "{\\s*\\n"],
 		description: "Should suggest object initialization",
@@ -129,7 +131,7 @@ greet(<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "import-from",
 		category: "Import Statement",
-		input: `import { useState } from <<<AUTOCOMPLETE_HERE>>>`,
+		input: `import { useState } from ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 25 },
 		expectedPatterns: ["'", '"', "'react'", '"react"'],
 		description: "Should complete import module name",
@@ -137,7 +139,7 @@ greet(<<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "import-curly-brace",
 		category: "Import Statement",
-		input: `import <<<AUTOCOMPLETE_HERE>>> from 'react'`,
+		input: `import ${CURSOR_MARKER} from 'react'`,
 		cursorPosition: { line: 0, character: 7 },
 		expectedPatterns: ["{", "React", "\\* as"],
 		description: "Should suggest import syntax options",
@@ -148,7 +150,7 @@ greet(<<<AUTOCOMPLETE_HERE>>>`,
 		name: "if-statement",
 		category: "Control Flow",
 		input: `const x = 10;
-if (x > 5) <<<AUTOCOMPLETE_HERE>>>`,
+if (x > 5) ${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 11 },
 		expectedPatterns: ["{", "{\\n", "{\\s*\\n\\s*console.log"],
 		description: "Should complete if statement body",
@@ -156,7 +158,7 @@ if (x > 5) <<<AUTOCOMPLETE_HERE>>>`,
 	{
 		name: "for-loop",
 		category: "Control Flow",
-		input: `for (let i = 0; i < 10; i++) <<<AUTOCOMPLETE_HERE>>>`,
+		input: `for (let i = 0; i < 10; i++) ${CURSOR_MARKER}`,
 		cursorPosition: { line: 0, character: 30 },
 		expectedPatterns: ["{", "{\\n", "{\\s*\\n\\s*console.log"],
 		description: "Should complete for loop body",
@@ -165,7 +167,7 @@ if (x > 5) <<<AUTOCOMPLETE_HERE>>>`,
 		name: "return-statement",
 		category: "Control Flow",
 		input: `function getValue() {
-  return <<<AUTOCOMPLETE_HERE>>>`,
+		return ${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 9 },
 		expectedPatterns: ["null", "undefined", "true", "false", "{", "\\["],
 		description: "Should suggest return value completions",
@@ -176,8 +178,8 @@ if (x > 5) <<<AUTOCOMPLETE_HERE>>>`,
 		name: "chained-methods",
 		category: "Complex",
 		input: `const result = [1, 2, 3]
-  .map(x => x * 2)
-  .<<<AUTOCOMPLETE_HERE>>>`,
+		.map(x => x * 2)
+		.${CURSOR_MARKER}`,
 		cursorPosition: { line: 2, character: 3 },
 		expectedPatterns: ["filter", "reduce", "forEach", "map"],
 		description: "Should suggest chained array methods",
@@ -186,9 +188,9 @@ if (x > 5) <<<AUTOCOMPLETE_HERE>>>`,
 		name: "nested-object",
 		category: "Complex",
 		input: `const config = {
-  server: {
-    port: 3000,
-    <<<AUTOCOMPLETE_HERE>>>`,
+		server: {
+		  port: 3000,
+		  ${CURSOR_MARKER}`,
 		cursorPosition: { line: 3, character: 4 },
 		expectedPatterns: ["host:", "hostname:", "}"],
 		description: "Should suggest nested object properties",
@@ -197,7 +199,7 @@ if (x > 5) <<<AUTOCOMPLETE_HERE>>>`,
 		name: "async-await",
 		category: "Complex",
 		input: `async function fetchData() {
-  const response = await <<<AUTOCOMPLETE_HERE>>>`,
+		const response = await ${CURSOR_MARKER}`,
 		cursorPosition: { line: 1, character: 25 },
 		expectedPatterns: ["fetch", "axios", "Promise"],
 		description: "Should suggest async operations",

+ 5 - 1
src/test-llm-autocompletion/tsconfig.json

@@ -11,7 +11,11 @@
 		"resolveJsonModule": true,
 		"allowImportingTsExtensions": true,
 		"noEmit": true,
-		"types": ["node"]
+		"types": ["node"],
+		"baseUrl": ".",
+		"paths": {
+			"vscode": ["./mock-vscode.ts"]
+		}
 	},
 	"include": ["**/*.ts"],
 	"exclude": ["node_modules"]

+ 0 - 19
src/test-llm-autocompletion/types.ts

@@ -1,19 +0,0 @@
-export enum UseCaseType {
-	AUTO_TRIGGER = "auto_trigger",
-	COMMENT_DRIVEN = "comment_driven",
-	ERROR_FIX = "error_fix",
-	INLINE_COMPLETION = "inline_completion",
-	NEW_LINE = "new_line",
-	SELECTION_REFACTOR = "selection_refactor",
-	USER_REQUEST = "user_request",
-}
-
-export interface GhostSuggestionContext {
-	document: any
-	range: any
-	recentOperations?: any[]
-	diagnostics?: any[]
-	openFiles?: any[]
-	userInput?: string
-	rangeASTNode?: any
-}