Add test host bridge server and refactor proto utilities (#5600)

- Extract getPackageDefinition() from loadProtoDescriptorSet() in proto-utils
- Add int64 encoding option to handle numbers properly
- Create test-hostbridge-server.ts with mock gRPC service implementations
- Add -h flag to runclinecore.sh to start test server
- Include testing.md documentation for cline rules
This commit is contained in:
Sarah Fortune
2025-08-15 18:07:45 +01:00
committed by GitHub
parent 8c49ce56f6
commit 44f370f295
4 changed files with 151 additions and 3 deletions
+7 -2
View File
@@ -23,9 +23,14 @@ export function getFqn(name) {
return typeNameToFQN.get(name)
}
export async function loadProtoDescriptorSet() {
export async function getPackageDefinition() {
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
const options = { longs: Number } // Encode int64 fields as numbers
return protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer, options)
}
export async function loadProtoDescriptorSet() {
const packageDefinition = await getPackageDefinition()
return grpc.loadPackageDefinition(packageDefinition)
}
+4
View File
@@ -4,6 +4,10 @@ set -eu #x
# This installs the cline-core app to the user's home directory,
# and starts the service.
if [[ "${1:-}" == "-h" ]]; then
./scripts/test-hostbridge-server.ts &
fi
CORE_DIR=~/.cline/core
INSTALL_DIR=$CORE_DIR/0.0.1
LOG_FILE=~/.cline/cline-core-service.log
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env npx tsx
import * as grpc from "@grpc/grpc-js"
import * as health from "grpc-health-check"
import { ReflectionService } from "@grpc/reflection"
import * as os from "os"
import { host } from "src/generated/grpc-js/index"
import { getPackageDefinition } from "./proto-utils.mjs"
export async function startTestHostBridgeServer() {
const server = new grpc.Server()
// Set up health check
const healthImpl = new health.HealthImplementation({ "": "SERVING" })
healthImpl.addToServer(server)
// Add host bridge services using the mock implementations
server.addService(host.WorkspaceServiceService, createMockService<host.WorkspaceServiceServer>("WorkspaceService"))
server.addService(host.WindowServiceService, createMockService<host.WindowServiceServer>("WindowService"))
server.addService(host.EnvServiceService, createMockService<host.EnvServiceServer>("EnvService"))
server.addService(host.DiffServiceService, createMockService<host.DiffServiceServer>("DiffService"))
server.addService(host.WatchServiceService, createMockService<host.WatchServiceServer>("WatchService"))
// Load package definition for reflection service
const packageDefinition = await getPackageDefinition()
// Filter service names to only include host services
const hostBridgeServiceNames = Object.keys(packageDefinition).filter(
(name) => name.startsWith("host.") || name.startsWith("grpc.health"),
)
const reflection = new ReflectionService(packageDefinition, {
services: hostBridgeServiceNames,
})
reflection.addToServer(server)
const bindAddress = process.env.HOST_BRIDGE_ADDRESS || `127.0.0.1:26041`
server.bindAsync(bindAddress, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) {
console.error(`Failed to bind test host bridge server to ${bindAddress}:`, err)
process.exit(1)
}
server.start()
console.log(`Test HostBridge gRPC server listening on ${bindAddress}`)
})
}
/**
* Creates a mock gRPC service implementation using Proxy
* @param serviceName Name of the service for logging
* @returns A proxy that implements the service interface
*/
function createMockService<T extends grpc.UntypedServiceImplementation>(serviceName: string): T {
const handler: ProxyHandler<T> = {
get(_target, prop) {
// Return a function that handles the gRPC call
return (call: any, callback: any) => {
console.log(`Hostbridge: ${serviceName}.${String(prop)} called with:`, call.request)
// Special cases that need specific return values
switch (prop) {
case "getWorkspacePaths":
callback(null, {
paths: ["/test-workspace"],
})
return
case "getMachineId":
callback(null, {
value: "fake-machine-id-" + os.hostname(),
})
return
case "clipboardReadText":
callback(null, {
value: "",
})
return
case "getWebviewHtml":
callback(null, {
html: "<html><body>Fake Webview</body></html>",
})
return
case "showTextDocument":
callback(null, {
document_path: call.request?.path || "",
view_column: 1,
is_active: true,
})
return
case "openDiff":
callback(null, {
diff_id: "fake-diff-" + Date.now(),
})
return
case "getDocumentText":
callback(null, {
content: "",
})
return
case "getOpenTabs":
case "getVisibleTabs":
case "showOpenDialogue":
callback(null, {
paths: [],
})
return
case "getDiagnostics":
callback(null, {
file_diagnostics: [],
})
return
// For streaming methods (like subscribeToFile)
case "subscribeToFile":
// Just end the stream immediately
call.end()
return
}
// Default: return empty object for all other methods
callback(null, {})
}
},
}
return new Proxy({} as T, handler)
}
if (require.main === module) {
startTestHostBridgeServer().catch((err) => {
console.error("Failed to start test host bridge server:", err)
process.exit(1)
})
}